    package flare.vis.controls
    {
        import flare.util.Displays;

        import flash.display.InteractiveObject;
        import flash.display.Stage;
        import flash.events.Event;
        import flash.events.MouseEvent;

        /**
         * Interactive control for panning and zooming a "camera". Any sprite can
         * be treated as a camera onto its drawing content and display list
         * children. The PanZoomControl allows you to manipulate a sprite's
         * transformation matrix (the <code>transform.matrix</code> property) to
         * simulate camera movements such as panning and zooming. To pan and
         * zoom over a collection of objects, simply add a PanZoomControl for
         * the sprite holding the collection.
         *
         * <pre>
         * var s:Sprite; // a sprite holding a collection of items
         * new PanZoomControl().attach(s); // attach pan and zoom controls to the sprite
         * </pre>
         * <p>Once a PanZoomControl has been created, panning is performed by
         * clicking and dragging. Zooming is performed either by scrolling the
         * mouse wheel or by clicking and dragging vertically while the control key
         * is pressed.</p>
         *
         * <p>By default, the PanZoomControl attaches itself to the
         * <code>stage</code> to listen for mouse events. This works fine if there
         * is only one collection of objects in the display list, but can cause
         * trouble if you want to have multiple collections that can be separately
         * panned and zoomed. The PanZoomControl constructor takes a second
         * argument that specifies a "hit area", a shape in the display list that
         * should be used to listen to the mouse events for panning and zooming.
         * For example, this could be a background sprite behind the zoomable
         * content, to which the "camera" sprite could be added. One can then set
         * the <code>scrollRect</code> property to add clipping bounds to the
         * panning and zooming region.</p>
         */
        public class PanZoomControl extends Control
        {
            private var px:Number, py:Number;
            private var dx:Number, dy:Number;
            private var mx:Number, my:Number;
            private var _drag:Boolean = false;

            private var _hit:InteractiveObject;
            private var _stage:Stage;

            /** The active hit area over which pan/zoom interactions can be performed. */
            public function get hitArea():InteractiveObject { return _hit; }
            public function set hitArea(hitArea:InteractiveObject):void {
                if (_hit != null) onRemove();
                _hit = hitArea;
                if (_object && _object.stage != null) onAdd();
            }

            /**
             * Creates a new PanZoomControl.
             * @param hitArea a display object to use as the hit area for mouse
             *  events. For example, this could be a background region over which
             *  the panning and zooming should be done. If this argument is null,
             *  the stage will be used.
             */
            public function PanZoomControl(hitArea:InteractiveObject=null):void
            {
                _hit = hitArea;
            }

            /** @inheritDoc */
            public override function attach(obj:InteractiveObject):void
            {
                super.attach(obj);
                if (obj != null) {
                    obj.addEventListener(Event.ADDED_TO_STAGE, onAdd);
                    obj.addEventListener(Event.REMOVED_FROM_STAGE, onRemove);
                    if (obj.stage != null) onAdd();
                }
            }

            /** @inheritDoc */
            public override function detach():InteractiveObject
            {
                onRemove();
                _object.removeEventListener(Event.ADDED_TO_STAGE, onAdd);
                _object.removeEventListener(Event.REMOVED_FROM_STAGE, onRemove);
                return super.detach();
            }

            private function onAdd(evt:Event=null):void
            {
                _stage = _object.stage;
                if (_hit == null) {
                    _hit = _stage;
                }
                _hit.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
                _hit.addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel);
            }

            private function onRemove(evt:Event=null):void
            {
                _hit.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
                _hit.removeEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel);
            }

            private function onMouseDown(event:MouseEvent) : void
            {
                if (_stage == null) return;
                if (_hit == _object && event.target != _hit) return;

                _stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
                _stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);

                px = mx = event.stageX;
                py = my = event.stageY;
                _drag = true;
            }

            private function onMouseMove(event:MouseEvent) : void
            {
                if (!_drag) return;

                var x:Number = event.stageX;
                var y:Number = event.stageY;

                if (!event.ctrlKey) {
                    dx = dy = NaN;
                    Displays.panBy(_object, x-mx, y-my);
                } else {
                    if (isNaN(dx)) {
                        dx = event.stageX;
                        dy = event.stageY;
                    }
                    var dz:Number = 1 + (y-my)/100;
                    Displays.zoomBy(_object, dz, dx, dy);
                }
                mx = x;
                my = y;
            }

            private function onMouseUp(event:MouseEvent) : void
            {
                dx = dy = NaN;
                _drag = false;
                _stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
                _stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
            }

            private function onMouseWheel(event:MouseEvent) : void
            {
                var dw:Number = 1.1 * event.delta;
                var dz:Number = dw < 0 ? 1/Math.abs(dw) : dw;
                Displays.zoomBy(_object, dz);
            }

        } // end of class PanZoomControl
    }