PageRenderTime 78ms CodeModel.GetById 50ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/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 | 1aa4561911210caef2437c555994c920 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>&gt;</td><td>X</td><td>Pan down</td></tr>
 51 * </table>
 52 * </p>
 53 *
 54 * <p>
 55 * <strong>Notes:</strong>&#xA0;
 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 pixels 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) {
215		throw new ArgumentError ("Pan increment must be 0 or greater: " + 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 = 0;
234	rubberband.y = 0;
235	rubberband.visible = false;
236	map.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}