PageRenderTime 73ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/examples/HelloRubberBandCtrl/src/com/google/maps/extras/rubberbandctrl/RubberBandCtrl.as

http://gmaps-utility-library-flash.googlecode.com/
ActionScript | 631 lines | 515 code | 13 blank | 103 comment | 7 complexity | d9ac0716687e799ae520ea92ad29ae07 MD5 | raw file
  1. package com.google.maps.extras.rubberbandctrl
  2. {
  3. import com.google.maps.LatLng;
  4. import com.google.maps.LatLngBounds;
  5. import com.google.maps.Map;
  6. import com.google.maps.MapEvent;
  7. import com.google.maps.MapMouseEvent;
  8. import com.google.maps.MapMoveEvent;
  9. import flash.display.Shape;
  10. import flash.errors.IllegalOperationError;
  11. import flash.events.KeyboardEvent;
  12. import flash.geom.Point;
  13. import flash.ui.Mouse;
  14. /**
  15. * The RubberBandCtrl class works with <em>Google Maps API for Flash</em> to
  16. * provide a user with an efficient way to zoom in to a specific area on the
  17. * map. This class also offers convenient keyboard shortcuts (with support for
  18. * left and right-handed users) to rewind to previous map views, zoom out, as
  19. * well as pan in any of four directions.
  20. *
  21. * <p>
  22. * To zoom in to a specific area on the map, a user would normally click the
  23. * map and drag it to its desired center point, and then move the mouse to the
  24. * upper-left corner of the map to select the appropriate zoom level (a few
  25. * tries might be necessary to get the right fit). RubberBandCtrl provides
  26. * a more direct way to perform this task, as described below:
  27. * <ul>
  28. * <li>Hold a <em>Shift</em> key (the cursor changes to a cross).</li>
  29. * <li>Click and hold the left mouse button to anchor the rubber band to the map.</li>
  30. * <li>Drag the mouse to stretch the rubber band.</li>
  31. * <li>Release the mouse button to snap the map to the area enclosed by the
  32. * rubber band, or release the <em>Shift</em> key to cancel.</li>
  33. * </ul>
  34. * </p>
  35. *
  36. * <p>
  37. * The <em>Shift</em> key can also be used in conjunction with any of the keys
  38. * below to perform other kinds of map navigation:
  39. *
  40. * <table>
  41. * <tr><th>Left-hand<br/>Key</th><th>Right-hand<br/>Key</th><th>Action</th></tr>
  42. * <tr><td>?</td><td>Z</td><td>Zoom out one level</td></tr>
  43. * <tr><td>{</td><td>Q</td><td>Rewind to previous map view</td></tr>
  44. * <tr><td>:</td><td>A</td><td>Pan left</td></tr>
  45. * <tr><td>"</td><td>S</td><td>Pan right</td></tr>
  46. * <tr><td>L</td><td>D</td><td>Pan up</td></tr>
  47. * <tr><td>&gt;</td><td>X</td><td>Pan down</td></tr>
  48. * </table>
  49. * </p>
  50. *
  51. * <p>
  52. * <strong>Notes:</strong>&#xA0;
  53. * <ul>
  54. * <li>RubberBandCtrl is active only when the map has keyboard focus, which might require the user to first click the map.</li>
  55. * <li>Adobe Flash security policy disables RubberBandCtrl when the map is in full-screen mode.</li>
  56. * </ul>
  57. * </p>
  58. *
  59. * <p>
  60. * <strong>Contacts:</strong>
  61. * <ul>
  62. * <li>To contact the author of RubberBandCtrl, email kevin.macdonald AT thinkwrap DOT com</li>
  63. * <li>Visit <code>http://www.spatialdatabox.com</code> for other Google Flash Map demos and related information.</li>
  64. * <li>For updates to RubberBandCtrl, please visit <code>http://code.google.com/p/gmaps-utility-library-flash</code></li>
  65. * </ul>
  66. * </p>
  67. *
  68. * <p>
  69. * <strong>Copyright Notice:</strong><br>
  70. * <br>
  71. * Copyright 2009 Kevin Macdonald<br>
  72. * <br>
  73. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at<br>
  74. * <br>
  75. * http://www.apache.org/licenses/LICENSE-2.0<br>
  76. * <br>
  77. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.<br>
  78. * </p>
  79. */
  80. public class RubberBandCtrl
  81. {
  82. /**
  83. * Creates an empty rubber band control object, which must be assigned to a Google Map via <code>enable ()</code>.
  84. *
  85. * @example The following example shows how to assign a Google Map object to a RubberBandCtrl object:
  86. * <listing version="3.0">
  87. * :
  88. * map.addEventListener (MapEvent.MAP_READY, onMapReady);
  89. * :
  90. *
  91. * public function onMapReady (event:MapEvent):void
  92. * {
  93. * rbCtrl = new RubberBandCtrl ();
  94. * rbCtrl.enable (map);
  95. * }
  96. *
  97. * const map:Map = new Map ();
  98. * var rbCtrl:RubberBandCtrl = null;
  99. * </listing>
  100. */
  101. public function RubberBandCtrl ():void
  102. {
  103. }
  104. /**
  105. * Call this method once, ideally within the MapEvent.MAP_READY handler, to add
  106. * RubberBandCtrl functionality to a Google Map.
  107. *
  108. * @param googleMap The map to receive RubberBandCtrl functionality.
  109. */
  110. public function enable (googleMap:Map):void
  111. {
  112. if (googleMap == null) {
  113. throw new ArgumentError ("Map parameter is null. (Hint: create Map object first, then call enable ().");
  114. }
  115. if (map != null) {
  116. throw new IllegalOperationError ("RubberBandCtrl is already enabled.");
  117. }
  118. map = googleMap;
  119. crosshairCursor = new CrosshairCursor (map);
  120. setRightHandedKeyCodes ();
  121. addEventHandlers ();
  122. initRubberBandOverlay ();
  123. initZoomHistory ();
  124. // Map has a positional state to save, only if it has been loaded.
  125. if (map.isLoaded ()) {
  126. pushMapStateToHistory ();
  127. }
  128. }
  129. /**
  130. * Remove RubberBandCtrl functionality from a Google Map.
  131. */
  132. public function disable ():void
  133. {
  134. if (map == null) {
  135. throw new IllegalOperationError ("There is no map to be disabled. (Hint: did you call enable () first?)");
  136. }
  137. removeEventHandlers ();
  138. disableRubberBandOverlay ();
  139. initZoomHistory ();
  140. crosshairCursor = null;
  141. map = null;
  142. }
  143. /**
  144. * Set key codes for right handed users, who operate the mouse with the right
  145. * hand, and keyboard with the left.
  146. */
  147. public function setRightHandedKeyCodes ():void
  148. {
  149. zoomCharCode = 'Z'.charCodeAt (0);
  150. rewindViewCharCode = 'Q'.charCodeAt (0);
  151. panLeftCharCode = 'A'.charCodeAt (0);
  152. panRightCharCode = 'S'.charCodeAt (0);
  153. panUpCharCode = 'D'.charCodeAt (0);
  154. panDownCharCode = 'X'.charCodeAt (0);
  155. }
  156. /**
  157. * Set key codes for left handed users, who operate the mouse with the left
  158. * hand, and keyboard with the right.
  159. */
  160. public function setLeftHandedKeyCodes ():void
  161. {
  162. zoomCharCode = '?'.charCodeAt (0);
  163. rewindViewCharCode = '{'.charCodeAt (0);
  164. panLeftCharCode = ':'.charCodeAt (0);
  165. panRightCharCode = '"'.charCodeAt (0);
  166. panUpCharCode = 'L'.charCodeAt (0);
  167. panDownCharCode = '>'.charCodeAt (0);
  168. }
  169. /**
  170. * Change the color of the rubber band.
  171. *
  172. * @param rgb A hexadecimal color value of the rubber band; for example, red is 0xFF0000, green is 0x00FF00 and blue is 0x0000FF.
  173. */
  174. public function setRubberBandColor (rgb:uint):void
  175. {
  176. rubberBandColor = rgb;
  177. }
  178. /**
  179. * Set the thickness, in pixels, of the rubber band.
  180. *
  181. * @param thickness The thickness of the rubber band, in pixels.
  182. */
  183. public function setRubberBandThickness (pixels:int):void
  184. {
  185. if (pixels < 1 || pixels > 50) {
  186. throw new ArgumentError ("Invalid rubber band thickness: " + pixels);
  187. }
  188. rubberBandThicknessPx = pixels;
  189. }
  190. /**
  191. * Set the size, in pixels, for a keyboard initiated pan of the map.
  192. *
  193. * @param pixels The number of pixels to pan vertically or horizontally, when the user performs a pan action. Setting this value to 0 disables panning.
  194. */
  195. public function setPanIncrement (pixels:int):void
  196. {
  197. if (pixels < 0 || pixels > 1000) {
  198. throw new ArgumentError ("Increment exceeds limit of 0 .. 1000: " + pixels);
  199. }
  200. panIncrementPx = pixels;
  201. }
  202. private function addEventHandlers ():void
  203. {
  204. map.stage.addEventListener (KeyboardEvent.KEY_DOWN, keyDownHandler);
  205. map.stage.addEventListener (KeyboardEvent.KEY_UP, keyUpHandler);
  206. map.addEventListener (MapMouseEvent.MOUSE_DOWN, mouseDownHandler);
  207. map.addEventListener (MapMouseEvent.MOUSE_UP, mouseUpHandler);
  208. map.addEventListener (MapMouseEvent.MOUSE_MOVE, mapMouseMoveHandler);
  209. map.addEventListener (MapMouseEvent.ROLL_OUT, rollOutHandler);
  210. map.addEventListener (MapMoveEvent.MOVE_END, moveEndHandler);
  211. }
  212. private function initRubberBandOverlay ():void
  213. {
  214. rubberband.x = map.x;
  215. rubberband.y = map.y;
  216. rubberband.visible = false;
  217. map.stage.addChild (rubberband);
  218. }
  219. private function removeEventHandlers ():void
  220. {
  221. map.stage.removeEventListener (KeyboardEvent.KEY_DOWN, keyDownHandler);
  222. map.stage.removeEventListener (KeyboardEvent.KEY_UP, keyUpHandler);
  223. map.removeEventListener (MapMouseEvent.MOUSE_DOWN, mouseDownHandler);
  224. map.removeEventListener (MapMouseEvent.MOUSE_UP, mouseUpHandler);
  225. map.removeEventListener (MapMouseEvent.MOUSE_MOVE, mapMouseMoveHandler);
  226. map.removeEventListener (MapMouseEvent.ROLL_OUT, rollOutHandler);
  227. map.removeEventListener (MapMoveEvent.MOVE_END, moveEndHandler);
  228. }
  229. private function disableRubberBandOverlay ():void
  230. {
  231. map.stage.removeChild (rubberband);
  232. }
  233. // EVENT HANDLERS
  234. private function keyDownHandler (event:KeyboardEvent):void
  235. {
  236. if (event.shiftKey && ! prevShiftKeyDown) {
  237. // On Windows, holding down the Shift or character keys will generate
  238. // a continous stream of KEY_DOWN events. Must act on only first
  239. // occurrence of such event.
  240. doShiftDown ();
  241. }
  242. if (event.charCode != 0) {
  243. doKeyDown (event.charCode);
  244. }
  245. prevShiftKeyDown = event.shiftKey;
  246. }
  247. private function keyUpHandler (event:KeyboardEvent):void
  248. {
  249. if (! event.shiftKey && prevShiftKeyDown) {
  250. doShiftUp ();
  251. }
  252. prevShiftKeyDown = event.shiftKey;
  253. }
  254. private function mouseDownHandler (event:MapMouseEvent):void
  255. {
  256. doMouseDown ();
  257. }
  258. private function mouseUpHandler (event:MapMouseEvent):void
  259. {
  260. cursorPos = map.fromLatLngToViewport (event.latLng);
  261. doMouseUp ();
  262. }
  263. private function mapMouseMoveHandler (event:MapMouseEvent):void
  264. {
  265. cursorPos = map.fromLatLngToViewport (event.latLng);
  266. doMouseMove ();
  267. }
  268. private function moveEndHandler (mapEvent:MapEvent):void
  269. {
  270. pushMapStateToHistory ();
  271. }
  272. private function rollOutHandler (event:MapMouseEvent):void
  273. {
  274. cursorPos = null;
  275. showStandardCursor ();
  276. }
  277. // STATE TRANSITIONS
  278. private function doShiftDown ():void
  279. {
  280. switch (state) {
  281. case ST_IDLE:
  282. if (! mouseDown && cursorPos != null) {
  283. state = ST_WATCH;
  284. showCrosshairCursor ();
  285. }
  286. break;
  287. case ST_WATCH:
  288. break;
  289. case ST_DRAW:
  290. break;
  291. default:
  292. throw new IllegalOperationError ("Unknown state=" + state);
  293. }
  294. }
  295. private function doShiftUp ():void
  296. {
  297. switch (state) {
  298. case ST_IDLE:
  299. // Happens if user had clicked mouse, pressed shift (which doesn't
  300. // cause a transition to ST_WATCH) then released shift.
  301. break;
  302. case ST_WATCH:
  303. cancelDraw ();
  304. state = ST_IDLE;
  305. break;
  306. case ST_DRAW:
  307. cancelDraw ();
  308. state = ST_IDLE;
  309. break;
  310. default:
  311. throw new IllegalOperationError ("Unknown state=" + state);
  312. }
  313. }
  314. private function doKeyDown (key:uint):void
  315. {
  316. switch (state) {
  317. case ST_IDLE:
  318. break;
  319. case ST_WATCH:
  320. processKeyAction (key);
  321. break;
  322. case ST_DRAW:
  323. break;
  324. default:
  325. throw new IllegalOperationError ("Unknown state=" + state);
  326. }
  327. }
  328. private function doMouseDown ():void
  329. {
  330. mouseDown = true;
  331. switch (state) {
  332. case ST_IDLE:
  333. break;
  334. case ST_WATCH:
  335. // Anchor rubber band
  336. map.disableDragging ();
  337. rubberBandStartPt = cursorPos;
  338. rubberband.graphics.clear ();
  339. rubberband.visible = true;
  340. state = ST_DRAW;
  341. break;
  342. case ST_DRAW:
  343. throw new IllegalOperationError ("Invalid state");
  344. break;
  345. default:
  346. throw new IllegalOperationError ("Unknown state=" + state);
  347. }
  348. }
  349. private function doMouseUp ():void
  350. {
  351. mouseDown = false;
  352. switch (state) {
  353. case ST_IDLE:
  354. break;
  355. case ST_WATCH:
  356. break;
  357. case ST_DRAW:
  358. panAndZoom ();
  359. cancelDraw ();
  360. state = ST_WATCH;
  361. break;
  362. default:
  363. throw new IllegalOperationError ("Unknown state=" + state);
  364. }
  365. }
  366. private function doMouseMove ():void
  367. {
  368. switch (state) {
  369. case ST_IDLE:
  370. break;
  371. case ST_WATCH:
  372. drawRubberBandCursor ();
  373. break;
  374. case ST_DRAW:
  375. drawRubberBandCursor ();
  376. drawRubberBand ();
  377. break;
  378. default:
  379. throw new IllegalOperationError ("Unknown state=" + state);
  380. }
  381. }
  382. // SUPPORTING FUNCTIONS FOR KEY & MOUSE EVENTS
  383. private function drawRubberBandCursor ():void
  384. {
  385. Mouse.hide ();
  386. crosshairCursor.show (cursorPos);
  387. }
  388. // Draw a rectangle on the 'rubberband' overlay between 'rubberBandStartPt' and 'cursorPos'.
  389. private function drawRubberBand ():void
  390. {
  391. rubberband.graphics.clear ();
  392. rubberband.graphics.lineStyle (rubberBandThicknessPx, rubberBandColor);
  393. rubberband.graphics.moveTo (rubberBandStartPt.x, rubberBandStartPt.y);
  394. rubberband.graphics.lineTo (cursorPos.x, rubberBandStartPt.y);
  395. rubberband.graphics.lineTo (cursorPos.x, cursorPos.y);
  396. rubberband.graphics.lineTo (rubberBandStartPt.x, cursorPos.y);
  397. rubberband.graphics.lineTo (rubberBandStartPt.x, rubberBandStartPt.y);
  398. }
  399. private function cancelDraw ():void
  400. {
  401. // Restore standard map behavior
  402. showStandardCursor ();
  403. map.enableDragging ();
  404. // Remove rubberband image over map.
  405. rubberband.visible = false;
  406. rubberBandStartPt = null;
  407. }
  408. private function showStandardCursor ():void
  409. {
  410. crosshairCursor.hide ();
  411. Mouse.show ();
  412. }
  413. private function showCrosshairCursor ():void
  414. {
  415. Mouse.hide ();
  416. if (cursorPos != null) {
  417. crosshairCursor.show (cursorPos);
  418. }
  419. }
  420. // Take rectangle drawn, if any, and pan & zoom map such that the rectangle is
  421. // the map's new viewport.
  422. private function panAndZoom ():void
  423. {
  424. // Ignore, if no rubber band exists.
  425. if (rubberBandStartPt == null) {
  426. return;
  427. }
  428. // Get coordinates of viewport.
  429. const left:int = Math.min (rubberBandStartPt.x, cursorPos.x);
  430. const right:int = Math.max (rubberBandStartPt.x, cursorPos.x);
  431. const top:int = Math.min (rubberBandStartPt.y, cursorPos.y);
  432. const bottom:int = Math.max (rubberBandStartPt.y, cursorPos.y);
  433. if (left == right || top == bottom) {
  434. return;
  435. }
  436. // Centerpoint of pan
  437. const centerPt:Point = new Point ((left + right) / 2, (top + bottom) / 2);
  438. const centerLatLng:LatLng = map.fromViewportToLatLng (centerPt);
  439. // Zoom
  440. const swLatLng:LatLng = map.fromViewportToLatLng (new Point (left, bottom));
  441. const neLatLng:LatLng = map.fromViewportToLatLng (new Point (right, top));
  442. const bounds:LatLngBounds = new LatLngBounds (swLatLng, neLatLng);
  443. const zoom:Number = map.getBoundsZoomLevel (bounds);
  444. // Perform pan & zoom
  445. map.setCenter (centerLatLng, zoom);
  446. }
  447. private function processKeyAction (charCode:uint):void
  448. {
  449. const mapCenter:Point = map.fromLatLngToViewport (map.getCenter ());
  450. if (charCode == zoomCharCode) {
  451. map.setZoom (Math.max (0, map.getZoom () - 1));
  452. } else if (charCode == rewindViewCharCode) {
  453. popMapStateFromHistory ();
  454. } else if (charCode == panLeftCharCode) {
  455. map.setCenter (map.fromViewportToLatLng (new Point (Math.max (0, mapCenter.x - panIncrementPx), mapCenter.y)));
  456. } else if (charCode == panRightCharCode) {
  457. map.setCenter (map.fromViewportToLatLng (new Point (mapCenter.x + panIncrementPx, mapCenter.y)));
  458. } else if (charCode == panUpCharCode) {
  459. map.setCenter (map.fromViewportToLatLng (new Point (mapCenter.x, Math.max (0, mapCenter.y - panIncrementPx))));
  460. } else if (charCode == panDownCharCode) {
  461. map.setCenter (map.fromViewportToLatLng (new Point (mapCenter.x, mapCenter.y + panIncrementPx)));
  462. }
  463. }
  464. // MAP STATE HISTORY FUNCTIONS
  465. private function initZoomHistory ():void
  466. {
  467. for (var i:int = 0; i < zoomHistory.length; ++i) {
  468. zoomHistory[i] = INVALID_ZOOM;
  469. }
  470. ignoreMoveEndEvent = false;
  471. historyCurrentIndex = 0;
  472. }
  473. private function pushMapStateToHistory ():void
  474. {
  475. if (ignoreMoveEndEvent) {
  476. ignoreMoveEndEvent = false;
  477. return;
  478. }
  479. historyCurrentIndex = (historyCurrentIndex + 1) % MAX_VIEWPORT_HISTORY;
  480. zoomHistory[historyCurrentIndex] = map.getZoom ();
  481. latLngHistory[historyCurrentIndex] = map.getCenter ();
  482. }
  483. // Pop top entry in history arrays and apply to current state of 'map'.
  484. private function popMapStateFromHistory ():void
  485. {
  486. const prev:int = (historyCurrentIndex - 1) % MAX_VIEWPORT_HISTORY;
  487. if (zoomHistory[prev] == INVALID_ZOOM) {
  488. return;
  489. }
  490. ignoreMoveEndEvent = true;
  491. map.setCenter (latLngHistory[prev], zoomHistory[prev]);
  492. zoomHistory[historyCurrentIndex] = INVALID_ZOOM;
  493. historyCurrentIndex = prev;
  494. }
  495. private static const DEFAULT_PAN_INCREMENT_PX:int = 100;
  496. private static const DEFAULT_RUBBER_BAND_COLOR:uint = 0x8080ff;
  497. private static const DEFAULT_RUBBER_BAND_THICKNESS_PX:int = 2;
  498. private static const MAX_VIEWPORT_HISTORY:int = 100;
  499. private static const INVALID_ZOOM:int = -1;
  500. // States of user interaction with the map.
  501. // Waiting for user to press shift key.
  502. private static const ST_IDLE:uint = 100;
  503. // User has pressed shift key, cursor has changed shape, waiting for user to
  504. // click mouse.
  505. private static const ST_WATCH:uint = 101;
  506. // Rubber band is visible and is tracking mouse movement.
  507. private static const ST_DRAW:uint = 102;
  508. // Current state 'ST_*'.
  509. private var state:uint = ST_IDLE;
  510. // State of mouse button.
  511. private var mouseDown:Boolean = false;
  512. // Map to apply DragZoom functionality.
  513. private var map:Map = null;
  514. // Current cursor position
  515. private var cursorPos:Point = null;
  516. // Rubber band, which is drawn as a rectangle, anchored at one edge to
  517. // 'rubberBandStartPt'.
  518. private const rubberband:Shape = new Shape ();
  519. private var rubberBandStartPt:Point = null;
  520. // Rubber band properties.
  521. private var rubberBandColor:uint = DEFAULT_RUBBER_BAND_COLOR;
  522. private var rubberBandThicknessPx:int = DEFAULT_RUBBER_BAND_THICKNESS_PX;
  523. // Navigation key bindings
  524. private var zoomCharCode:int = 0;
  525. private var rewindViewCharCode:int = 0;
  526. private var panLeftCharCode:int = 0;
  527. private var panRightCharCode:int = 0;
  528. private var panUpCharCode:int = 0;
  529. private var panDownCharCode:int = 0;
  530. // How far to pan, in pixels, vertically or horizontally
  531. private var panIncrementPx:int = DEFAULT_PAN_INCREMENT_PX;
  532. // Previous key event. Used to detect and discard key repeats.
  533. private var prevShiftKeyDown:Boolean = false;
  534. // Map position history, organized as a circular buffer.
  535. private var zoomHistory:Array = new Array (MAX_VIEWPORT_HISTORY);
  536. private var latLngHistory:Array = new Array (MAX_VIEWPORT_HISTORY);
  537. private var historyCurrentIndex:int = 0;
  538. private var ignoreMoveEndEvent:Boolean;
  539. // Cursor to display, when shift key is held.
  540. private var crosshairCursor:CrosshairCursor = null;
  541. }
  542. }