PageRenderTime 28ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/com/modestmaps/overlays/MarkerClip.as

https://github.com/pombredanne/Elastic-Lists
ActionScript | 473 lines | 330 code | 74 blank | 69 comment | 43 complexity | 29b96496b33dc1c56deb9115c319af1f MD5 | raw file
  1. package com.modestmaps.overlays
  2. {
  3. import com.modestmaps.Map;
  4. import com.modestmaps.core.Coordinate;
  5. import com.modestmaps.events.MapEvent;
  6. import com.modestmaps.events.MarkerEvent;
  7. import com.modestmaps.geo.Location;
  8. import com.modestmaps.mapproviders.IMapProvider;
  9. import flash.display.DisplayObject;
  10. import flash.display.Sprite;
  11. import flash.events.Event;
  12. import flash.events.MouseEvent;
  13. import flash.geom.Point;
  14. import flash.utils.Dictionary;
  15. import flash.utils.clearTimeout;
  16. import flash.utils.setTimeout;
  17. [Event(name="markerRollOver", type="com.modestmaps.events.MarkerEvent")]
  18. [Event(name="markerRollOut", type="com.modestmaps.events.MarkerEvent")]
  19. [Event(name="markerClick", type="com.modestmaps.events.MarkerEvent")]
  20. public class MarkerClip extends Sprite
  21. {
  22. public static const DEFAULT_ZOOM_TOLERANCE:int = 4;
  23. protected var map:Map;
  24. protected var drawCoord:Coordinate;
  25. protected var locations:Dictionary = new Dictionary();
  26. protected var coordinates:Dictionary = new Dictionary();
  27. protected var markers:Array = []; // all markers
  28. protected var markersByName:Object = {};
  29. /** enable this if you want intermediate zooming steps to
  30. * stretch your graphics instead of reprojecting the points
  31. * it's useful for polygons, but for points
  32. * it looks worse and probably isn't faster, but there it is :) */
  33. public var scaleZoom:Boolean = false;
  34. /** if autoCache is true, we turn on cacheAsBitmap while panning, but off while zooming */
  35. public var autoCache:Boolean = true;
  36. /** if scaleZoom is true, this is how many zoom levels you
  37. * can zoom by before things will be reprojected regardless */
  38. public var zoomTolerance:Number = DEFAULT_ZOOM_TOLERANCE;
  39. // enable this if you want marker locations snapped to pixels
  40. public var snapToPixels:Boolean = false;
  41. // the function used to sort the markers array before re-ordering them
  42. // on the z plane (by child index)
  43. public var markerSortFunction:Function = sortMarkersByYPosition;
  44. // the projection of the current map's provider
  45. // if this changes we need to recache coordinates
  46. protected var previousGeometry:String;
  47. // setting this.dirty = true will redraw an MapEvent.RENDERED
  48. protected var _dirty:Boolean;
  49. /**
  50. * This is the function provided to markers.sort() in order to determine which
  51. * markers should go in front of the others. The default behavior is to place
  52. * markers further down on the screen (with higher y values) frontmost. You
  53. * can modify this behavior by specifying a different value for
  54. * MarkerClip.markerSortFunction
  55. */
  56. public static function sortMarkersByYPosition(a:DisplayObject, b:DisplayObject):int
  57. {
  58. var diffY:Number = a.y - b.y;
  59. return (diffY > 0) ? 1 : (diffY < 0) ? -1 : 0;
  60. }
  61. public function MarkerClip(map:Map)
  62. {
  63. // client code can listen to mouse events on this clip
  64. // to get all events bubbled up from the markers
  65. buttonMode = false;
  66. mouseEnabled = false;
  67. mouseChildren = true;
  68. this.map = map;
  69. this.x = map.getWidth() / 2;
  70. this.y = map.getHeight() / 2;
  71. previousGeometry = map.getMapProvider().geometry();
  72. map.addEventListener(MapEvent.START_ZOOMING, onMapStartZooming);
  73. map.addEventListener(MapEvent.STOP_ZOOMING, onMapStopZooming);
  74. map.addEventListener(MapEvent.ZOOMED_BY, onMapZoomedBy);
  75. map.addEventListener(MapEvent.START_PANNING, onMapStartPanning);
  76. map.addEventListener(MapEvent.STOP_PANNING, onMapStopPanning);
  77. map.addEventListener(MapEvent.PANNED, onMapPanned);
  78. map.addEventListener(MapEvent.RESIZED, onMapResized);
  79. map.addEventListener(MapEvent.EXTENT_CHANGED, onMapExtentChanged);
  80. map.addEventListener(MapEvent.RENDERED, updateClips);
  81. map.addEventListener(MapEvent.MAP_PROVIDER_CHANGED, onMapProviderChanged);
  82. // these were previously in Map, but now MarkerEvents bubble it makes more sense to have them here
  83. addEventListener( MouseEvent.CLICK, onMarkerClick );
  84. addEventListener( MouseEvent.ROLL_OVER, onMarkerRollOver, true );
  85. addEventListener( MouseEvent.ROLL_OUT, onMarkerRollOut, true );
  86. addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
  87. }
  88. public function getMarkerCount():int
  89. {
  90. return markers.length;
  91. }
  92. override public function set x(value:Number):void
  93. {
  94. super.x = snapToPixels ? Math.round(value) : value;
  95. }
  96. override public function set y(value:Number):void
  97. {
  98. super.y = snapToPixels ? Math.round(value) : value;
  99. }
  100. protected function onAddedToStage(event:Event):void
  101. {
  102. //addEventListener(Event.RENDER, updateClips);
  103. dirty = true;
  104. updateClips();
  105. removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
  106. addEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
  107. }
  108. protected function onRemovedFromStage(event:Event):void
  109. {
  110. //removeEventListener(Event.RENDER, updateClips);
  111. removeEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
  112. addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
  113. }
  114. public function attachMarker(marker:DisplayObject, location:Location):void
  115. {
  116. if (markers.indexOf(marker) == -1)
  117. {
  118. locations[marker] = location.clone();
  119. coordinates[marker] = map.getMapProvider().locationCoordinate(location);
  120. markersByName[marker.name] = marker;
  121. markers.push(marker);
  122. var added:Boolean = updateClip(marker);
  123. if (added) {
  124. requestSort(true);
  125. }
  126. }
  127. }
  128. protected function markerInBounds(marker:DisplayObject, w:Number, h:Number):Boolean
  129. {
  130. return marker.x > -w / 2 && marker.x < w / 2 &&
  131. marker.y > -h / 2 && marker.y < h / 2;
  132. }
  133. public function getMarker(id:String):DisplayObject
  134. {
  135. return markersByName[id] as DisplayObject;
  136. }
  137. public function getMarkerLocation( marker:DisplayObject ) : Location {
  138. return locations[marker];
  139. }
  140. public function hasMarker(marker:DisplayObject):Boolean
  141. {
  142. return markers.indexOf(marker) != -1;
  143. }
  144. public function setMarkerLocation(marker:DisplayObject, location:Location):void
  145. {
  146. locations[marker] = new Location(location.lat, location.lon);
  147. coordinates[marker] = map.getMapProvider().locationCoordinate(location);
  148. sortMarkers();
  149. dirty = true;
  150. }
  151. public function removeMarker(id:String):void
  152. {
  153. var marker:DisplayObject = getMarker(id);
  154. if (marker)
  155. {
  156. removeMarkerObject(marker);
  157. }
  158. }
  159. public function removeMarkerObject(marker:DisplayObject):void
  160. {
  161. if (this.contains(marker)) {
  162. removeChild(marker);
  163. }
  164. var index:int = markers.indexOf(marker);
  165. if (index >= 0) {
  166. markers.splice(index,1);
  167. }
  168. delete locations[marker];
  169. delete coordinates[marker];
  170. delete markersByName[marker.name];
  171. }
  172. // removeAllMarkers was implemented on trunk
  173. // meanwhile clearMarkers arrived in the tweening branch
  174. // let's go with the body from clearMarkers because it's shorter
  175. public function removeAllMarkers():void
  176. {
  177. while (markers.length > 0) {
  178. var marker:DisplayObject = markers.pop();
  179. removeMarkerObject(marker);
  180. }
  181. }
  182. public function updateClips(event:Event=null):void
  183. {
  184. if (!dirty) {
  185. return;
  186. }
  187. var center:Coordinate = map.grid.centerCoordinate;
  188. if (center.equalTo(drawCoord)) {
  189. dirty = false;
  190. return;
  191. }
  192. drawCoord = center.copy();
  193. this.x = map.getWidth() / 2;
  194. this.y = map.getHeight() / 2;
  195. if (scaleZoom) {
  196. scaleX = scaleY = 1.0;
  197. }
  198. var doSort:Boolean = false;
  199. for each (var marker:DisplayObject in markers)
  200. {
  201. doSort = updateClip(marker) || doSort; // wow! bad things did happen when this said doSort ||= updateClip(marker);
  202. }
  203. if (doSort) {
  204. requestSort(true);
  205. }
  206. dirty = false;
  207. }
  208. /** call this if you've made a change to the underlying map geometry such that
  209. * provider.locationCoordinate(location) will return a different coordinate */
  210. public function resetCoordinates():void
  211. {
  212. var provider:IMapProvider = map.getMapProvider();
  213. // I wish Array.map didn't require three parameters!
  214. for each (var marker:DisplayObject in markers) {
  215. coordinates[marker] = provider.locationCoordinate(locations[marker]);
  216. }
  217. dirty = true;
  218. }
  219. protected var sortTimer:uint;
  220. protected function requestSort(updateOrder:Boolean=false):void
  221. {
  222. // use a timer so we don't do this every single frame, otherwise
  223. // sorting markers and applying depths pretty much doubles the
  224. // time to run updateClips
  225. if (sortTimer) {
  226. clearTimeout(sortTimer);
  227. }
  228. sortTimer = setTimeout(sortMarkers, 50, updateOrder);
  229. }
  230. public function sortMarkers(updateOrder:Boolean=false):void
  231. {
  232. // only sort if we have a function:
  233. if (updateOrder && markerSortFunction != null)
  234. {
  235. markers = markers.sort(markerSortFunction, Array.NUMERIC);
  236. }
  237. // apply depths to maintain the order things were added in
  238. var index:uint = 0;
  239. for each (var marker:DisplayObject in markers)
  240. {
  241. if (contains(marker))
  242. {
  243. setChildIndex(marker, Math.min(index, numChildren - 1));
  244. index++;
  245. }
  246. }
  247. }
  248. /** returns true if the marker was added to the stage, so that updateClips or attachMarker can sort the markers */
  249. public function updateClip(marker:DisplayObject):Boolean
  250. {
  251. if (marker.visible)
  252. {
  253. // this method previously used the location of the marker
  254. // but map.locationPoint hands off to grid to grid.coordinatePoint
  255. // in the end so we may as well cache the first step
  256. var point:Point = map.grid.coordinatePoint(coordinates[marker], this);
  257. marker.x = snapToPixels ? Math.round(point.x) : point.x;
  258. marker.y = snapToPixels ? Math.round(point.y) : point.y;
  259. var w:Number = map.getWidth() * 2;
  260. var h:Number = map.getHeight() * 2;
  261. if (markerInBounds(marker, w, h))
  262. {
  263. if (!contains(marker))
  264. {
  265. addChild(marker);
  266. // notify the caller that we've added something and need to sort markers
  267. return true;
  268. }
  269. }
  270. else if (contains(marker))
  271. {
  272. removeChild(marker);
  273. // only need to sort if we've added something
  274. return false;
  275. }
  276. }
  277. return false;
  278. }
  279. ///// Events....
  280. protected function onMapExtentChanged(event:MapEvent):void
  281. {
  282. dirty = true;
  283. }
  284. protected function onMapPanned(event:MapEvent):void
  285. {
  286. if (drawCoord) {
  287. var p:Point = map.grid.coordinatePoint(drawCoord);
  288. this.x = p.x;
  289. this.y = p.y;
  290. }
  291. else {
  292. dirty = true;
  293. }
  294. }
  295. protected function onMapZoomedBy(event:MapEvent):void
  296. {
  297. if (autoCache) cacheAsBitmap = false;
  298. if (scaleZoom && drawCoord) {
  299. if (Math.abs(map.grid.zoomLevel - drawCoord.zoom) < zoomTolerance) {
  300. scaleX = scaleY = Math.pow(2, map.grid.zoomLevel - drawCoord.zoom);
  301. }
  302. else {
  303. dirty = true;
  304. }
  305. }
  306. else {
  307. dirty = true;
  308. }
  309. }
  310. protected function onMapStartPanning(event:MapEvent):void
  311. {
  312. // optimistically, we set this to true in case we're just moving
  313. if (autoCache) cacheAsBitmap = true;
  314. }
  315. protected function onMapStartZooming(event:MapEvent):void
  316. {
  317. // overrule onMapStartPanning if there's scaling involved
  318. if (autoCache) cacheAsBitmap = false;
  319. }
  320. protected function onMapStopPanning(event:MapEvent):void
  321. {
  322. // tidy up
  323. if (autoCache) cacheAsBitmap = false;
  324. dirty = true;
  325. }
  326. protected function onMapStopZooming(event:MapEvent):void
  327. {
  328. dirty = true;
  329. }
  330. protected function onMapResized(event:MapEvent):void
  331. {
  332. x = map.getWidth() / 2;
  333. y = map.getHeight() / 2;
  334. dirty = true;
  335. updateClips(); // force redraw because flash seems stingy about it
  336. }
  337. protected function onMapProviderChanged(event:MapEvent):void
  338. {
  339. var mapProvider:IMapProvider = map.getMapProvider();
  340. if (mapProvider.geometry() != previousGeometry)
  341. {
  342. resetCoordinates();
  343. previousGeometry = mapProvider.geometry();
  344. }
  345. }
  346. ///// Invalidations...
  347. protected function set dirty(d:Boolean):void
  348. {
  349. _dirty = d;
  350. if (d) {
  351. if (stage) stage.invalidate();
  352. }
  353. }
  354. protected function get dirty():Boolean
  355. {
  356. return _dirty;
  357. }
  358. ////// Marker Events...
  359. /**
  360. * Dispatches MarkerEvent.CLICK when a marker is clicked.
  361. *
  362. * The MarkerEvent includes a reference to the marker and its location.
  363. *
  364. * @see com.modestmaps.events.MarkerEvent.MARKER_CLICK
  365. */
  366. protected function onMarkerClick(event:MouseEvent):void
  367. {
  368. var marker:DisplayObject = event.target as DisplayObject;
  369. var location:Location = getMarkerLocation( marker );
  370. dispatchEvent( new MarkerEvent( MarkerEvent.MARKER_CLICK, marker, location, true) );
  371. }
  372. /**
  373. * Dispatches MarkerEvent.ROLL_OVER
  374. *
  375. * The MarkerEvent includes a reference to the marker and its location.
  376. *
  377. * @see com.modestmaps.events.MarkerEvent.MARKER_ROLL_OVER
  378. */
  379. protected function onMarkerRollOver(event:MouseEvent):void
  380. {
  381. var marker:DisplayObject = event.target as DisplayObject;
  382. var location:Location = getMarkerLocation( marker );
  383. dispatchEvent( new MarkerEvent( MarkerEvent.MARKER_ROLL_OVER, marker, location, true) );
  384. }
  385. /**
  386. * Dispatches MarkerEvent.ROLL_OUT
  387. *
  388. * The MarkerEvent includes a reference to the marker and its location.
  389. *
  390. * @see com.modestmaps.events.MarkerEvent.MARKER_ROLL_OUT
  391. */
  392. protected function onMarkerRollOut(event:MouseEvent):void
  393. {
  394. var marker:DisplayObject = event.target as DisplayObject;
  395. var location:Location = getMarkerLocation( marker );
  396. dispatchEvent( new MarkerEvent( MarkerEvent.MARKER_ROLL_OUT, marker, location, true) );
  397. }
  398. }
  399. }