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