package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.net.Socket;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.net.URLVariables;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.utils.setTimeout;

    public class swfcat extends Sprite
    {
        private const CIRRUS_URL:String = "rtmfp://tor-facilitator.bamsoftware.com";

        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "tor-facilitator.bamsoftware.com",
            port: 9002
        };

        /* Local Tor client to use in case of RTMFP connection. */
        private const LOCAL_TOR_CLIENT_ADDR:Object = {
            host: "127.0.0.1",
            port: 9002
        };

        private const MAX_NUM_PROXY_PAIRS:uint = 1;

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Bytes per second. Set to undefined to disable limit.
        public const RATE_LIMIT:Number = undefined;
        // Seconds.
        private const RATE_LIMIT_HISTORY:Number = 5.0;

        /* TextField for debug output. */
        private var output_text:TextField;

        /* UI shown when debug is off. */
        private var badge:Badge;

        /* Number of proxy pairs currently connected (up to
           MAX_NUM_PROXY_PAIRS). */
        private var num_proxy_pairs:int = 0;

        private var fac_addr:Object;

        public var rate_limit:RateLimit;

        public function puts(s:String):void
        {
            output_text.appendText(s + "\n");
            output_text.scrollV = output_text.maxScrollV;
        }

        public function swfcat()
        {
            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            output_text = new TextField();
            output_text.width = stage.stageWidth;
            output_text.height = stage.stageHeight;
            output_text.background = true;
            output_text.backgroundColor = 0x001f0f;
            output_text.textColor = 0x44cc44;

            badge = new Badge();

            if (RATE_LIMIT)
                rate_limit = new BucketRateLimit(RATE_LIMIT * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
            else
                rate_limit = new RateUnlimit();

            puts("Starting.");
            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;

            puts("Parameters loaded.");

            if (this.loaderInfo.parameters["debug"] || this.loaderInfo.parameters["client"])
                addChild(output_text);
            else
                addChild(badge);

            fac_addr = get_param_addr("facilitator", DEFAULT_FACILITATOR_ADDR);
            if (!fac_addr) {
                puts("Error: Facilitator spec must be in the form \"host:port\".");
                return;
            }

            if (this.loaderInfo.parameters["client"])
                client_main();
            else
                proxy_main();
        }

        /* Get an address structure from the given movie parameter, or the given
           default. Returns null on error. */
        private function get_param_addr(param:String, default_addr:Object):Object
        {
            var spec:String, addr:Object;

            spec = this.loaderInfo.parameters[param];
            if (spec)
                return parse_addr_spec(spec);
            else
                return default_addr;
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function proxy_main():void
        {
            var fac_url:String;
            var loader:URLLoader;

            if (num_proxy_pairs >= MAX_NUM_PROXY_PAIRS) {
                setTimeout(proxy_main, FACILITATOR_POLL_INTERVAL);
                return;
            }

            loader = new URLLoader();
            /* Get the x-www-form-urlencoded values. */
            loader.dataFormat = URLLoaderDataFormat.VARIABLES;
            loader.addEventListener(Event.COMPLETE, fac_complete);
            loader.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
            });
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
            });

            fac_url = "http://" + encodeURIComponent(fac_addr.host)
                + ":" + encodeURIComponent(fac_addr.port) + "/";
            puts("Facilitator: connecting to " + fac_url + ".");
            loader.load(new URLRequest(fac_url));
        }

        private function fac_complete(e:Event):void
        {
            var loader:URLLoader;
            var client_spec:String;
            var relay_spec:String;
            var proxy_pair:Object;

            setTimeout(proxy_main, FACILITATOR_POLL_INTERVAL);

            loader = e.target as URLLoader;
            client_spec = loader.data.client;
            if (client_spec == "") {
                puts("No clients.");
                return;
            } else if (!client_spec) {
                puts("Error: missing \"client\" in response.");
                return;
            }
            relay_spec = loader.data.relay;
            if (!relay_spec) {
                puts("Error: missing \"relay\" in response.");
                return;
            }
            puts("Facilitator: got client:\"" + client_spec + "\" "
                + "relay:\"" + relay_spec + "\".");

            try {
                proxy_pair = make_proxy_pair(client_spec, relay_spec);
            } catch (e:ArgumentError) {
                puts("Error: " + e);
                return;
            }
            proxy_pair.addEventListener(Event.COMPLETE, function(e:Event):void {
                proxy_pair.log("Complete.");
                num_proxy_pairs--;
                badge.proxy_end();
            });
            proxy_pair.connect();

            num_proxy_pairs++;
            badge.proxy_begin();
        }

        private function client_main():void
        {
            var rs:RTMFPSocket;

            rs = new RTMFPSocket(CIRRUS_URL);
            rs.addEventListener(Event.COMPLETE, function (e:Event):void {
                puts("Got RTMFP id " + rs.id);
                register(rs);
            });
            rs.addEventListener(RTMFPSocket.ACCEPT_EVENT, client_accept);

            rs.listen();
        }

        private function client_accept(e:Event):void {
            var rs:RTMFPSocket;
            var s_t:Socket;
            var proxy_pair:ProxyPair;

            rs = e.target as RTMFPSocket;
            s_t = new Socket();

            puts("Got RTMFP connection from " + rs.peer_id);

            proxy_pair = new ProxyPair(this, rs, function ():void {
                /* Do nothing; already connected. */
            }, s_t, function ():void {
                s_t.connect(LOCAL_TOR_CLIENT_ADDR.host, LOCAL_TOR_CLIENT_ADDR.port);
            });
            proxy_pair.connect();
        }

        private function register(rs:RTMFPSocket):void {
            var fac_url:String;
            var loader:URLLoader;
            var request:URLRequest;
            var variables:URLVariables;

            loader = new URLLoader();
            loader.addEventListener(Event.COMPLETE, function (e:Event):void {
                puts("Facilitator: registered.");
            });
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
                rs.close();
            });
            loader.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
                rs.close();
            });

            fac_url = "http://" + encodeURIComponent(fac_addr.host)
                + ":" + encodeURIComponent(fac_addr.port) + "/";
            request = new URLRequest(fac_url);
            request.method = URLRequestMethod.POST;
            request.data = new URLVariables;
            request.data["client"] = rs.id;

            puts("Facilitator: connecting to " + fac_url + ".");
            loader.load(request);
        }

        private function make_proxy_pair(client_spec:String, relay_spec:String):ProxyPair
        {
            var proxy_pair:ProxyPair;
            var addr_c:Object;
            var addr_r:Object;
            var s_c:*;
            var s_r:Socket;

            addr_r = swfcat.parse_addr_spec(relay_spec);
            if (!addr_r)
                throw new ArgumentError("Relay spec must be in the form \"host:port\".");

            addr_c = swfcat.parse_addr_spec(client_spec);
            if (addr_c) {
                s_c = new Socket();
                s_r = new Socket();
                proxy_pair = new ProxyPair(this, s_c, function ():void {
                    s_c.connect(addr_c.host, addr_c.port);
                }, s_r, function ():void {
                    s_r.connect(addr_r.host, addr_r.port);
                });
                proxy_pair.set_name("<" + addr_c.host + ":" + addr_c.port + ","
                    + addr_r.host + ":" + addr_r.port + ">");
                return proxy_pair;
            }

            if (client_spec.match(/^[0-9A-Fa-f]{64}$/)) {
                s_c = new RTMFPSocket(CIRRUS_URL);
                s_r = new Socket();
                proxy_pair = new ProxyPair(this, s_c, function ():void {
                    s_c.connect(client_spec);
                }, s_r, function ():void {
                    s_r.connect(addr_r.host, addr_r.port);
                });
                proxy_pair.set_name("<" + client_spec.substr(0, 4) + "...,"
                    + addr_r.host + ":" + addr_r.port + ">");
                return proxy_pair;
            }

            throw new ArgumentError("Can't parse client spec \"" + client_spec + "\".");
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private static function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.text.TextFormat;
import flash.text.TextField;
import flash.utils.getTimer;

class Badge extends flash.display.Sprite
{
    /* Number of proxy pairs currently connected. */
    private var num_proxy_pairs:int = 0;
    /* Number of proxy pairs ever connected. */
    private var total_proxy_pairs:int = 0;

    [Embed(source="badge.png")]
    private var BadgeImage:Class;
    private var tot_client_count_tf:TextField;
    private var tot_client_count_fmt:TextFormat;
    private var cur_client_count_tf:TextField;
    private var cur_client_count_fmt:TextFormat;

    public function Badge()
    {
        /* Setup client counter for badge. */
        tot_client_count_fmt = new TextFormat();
        tot_client_count_fmt.color = 0xFFFFFF;
        tot_client_count_fmt.align = "center";
        tot_client_count_fmt.font = "courier-new";
        tot_client_count_fmt.bold = true;
        tot_client_count_fmt.size = 10;
        tot_client_count_tf = new TextField();
        tot_client_count_tf.width = 20;
        tot_client_count_tf.height = 17;
        tot_client_count_tf.background = false;
        tot_client_count_tf.defaultTextFormat = tot_client_count_fmt;
        tot_client_count_tf.x=47;
        tot_client_count_tf.y=0;

        cur_client_count_fmt = new TextFormat();
        cur_client_count_fmt.color = 0xFFFFFF;
        cur_client_count_fmt.align = "center";
        cur_client_count_fmt.font = "courier-new";
        cur_client_count_fmt.bold = true;
        cur_client_count_fmt.size = 10;
        cur_client_count_tf = new TextField();
        cur_client_count_tf.width = 20;
        cur_client_count_tf.height = 17;
        cur_client_count_tf.background = false;
        cur_client_count_tf.defaultTextFormat = cur_client_count_fmt;
        cur_client_count_tf.x=47;
        cur_client_count_tf.y=6;

        addChild(new BadgeImage());
        addChild(tot_client_count_tf);
        addChild(cur_client_count_tf);

        /* Update the client counter on badge. */
        update_client_count();
    }

    public function proxy_begin():void
    {
        num_proxy_pairs++;
        total_proxy_pairs++;
        update_client_count();
    }

    public function proxy_end():void
    {
        num_proxy_pairs--;
        update_client_count();
    }

    private function update_client_count():void
    {
        /* Update total client count. */
        if (String(total_proxy_pairs).length == 1)
            tot_client_count_tf.text = "0" + String(total_proxy_pairs);
        else
            tot_client_count_tf.text = String(total_proxy_pairs);

        /* Update current client count. */
        cur_client_count_tf.text = "";
        for(var i:Number = 0; i < num_proxy_pairs; i++)
            cur_client_count_tf.appendText(".");
    }
}

class RateLimit
{
    public function RateLimit()
    {
    }

    public function update(n:Number):Boolean
    {
        return true;
    }

    public function when():Number
    {
        return 0.0;
    }

    public function is_limited():Boolean
    {
        return false;
    }
}

class RateUnlimit extends RateLimit
{
    public function RateUnlimit()
    {
    }

    public override function update(n:Number):Boolean
    {
        return true;
    }

    public override function when():Number
    {
        return 0.0;
    }

    public override function is_limited():Boolean
    {
        return false;
    }
}

class BucketRateLimit extends RateLimit
{
    private var amount:Number;
    private var capacity:Number;
    private var time:Number;
    private var last_update:uint;

    public function BucketRateLimit(capacity:Number, time:Number)
    {
        this.amount = 0.0;
        /* capacity / time is the rate we are aiming for. */
        this.capacity = capacity;
        this.time = time;
        this.last_update = getTimer();
    }

    private function age():void
    {
        var now:uint;
        var delta:Number;

        now = getTimer();
        delta = (now - last_update) / 1000.0;
        last_update = now;

        amount -= delta * capacity / time;
        if (amount < 0.0)
            amount = 0.0;
    }

    public override function update(n:Number):Boolean
    {
        age();
        amount += n;

        return amount <= capacity;
    }

    public override function when():Number
    {
        age();
        return (amount - capacity) / (capacity / time);
    }

    public override function is_limited():Boolean
    {
        age();
        return amount > capacity;
    }
}

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.net.Socket;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.net.URLVariables;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.utils.setTimeout;

    public class swfcat extends Sprite
    {
        private const RTMFP_URL:String = "rtmfp://tor-facilitator.bamsoftware.com";

        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "tor-facilitator.bamsoftware.com",
            port: 9002
        };

        /* Local Tor client to use in case of RTMFP connection. */
        private const LOCAL_TOR_CLIENT_ADDR:Object = {
            host: "127.0.0.1",
            port: 9002
        };

        private const MAX_NUM_PROXY_PAIRS:uint = 1;

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Bytes per second. Set to undefined to disable limit.
        public const RATE_LIMIT:Number = undefined;
        // Seconds.
        private const RATE_LIMIT_HISTORY:Number = 5.0;

        /* TextField for debug output. */
        private var output_text:TextField;

        /* UI shown when debug is off. */
        private var badge:Badge;

        /* Number of proxy pairs currently connected (up to
           MAX_NUM_PROXY_PAIRS). */
        private var num_proxy_pairs:int = 0;

        private var fac_addr:Object;

        public var rate_limit:RateLimit;

        public function puts(s:String):void
        {
            output_text.appendText(s + "\n");
            output_text.scrollV = output_text.maxScrollV;
        }

        public function swfcat()
        {
            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            output_text = new TextField();
            output_text.width = stage.stageWidth;
            output_text.height = stage.stageHeight;
            output_text.background = true;
            output_text.backgroundColor = 0x001f0f;
            output_text.textColor = 0x44cc44;

            badge = new Badge();

            if (RATE_LIMIT)
                rate_limit = new BucketRateLimit(RATE_LIMIT * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
            else
                rate_limit = new RateUnlimit();

            puts("Starting.");
            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;

            puts("Parameters loaded.");

            if (this.loaderInfo.parameters["debug"] || this.loaderInfo.parameters["client"])
                addChild(output_text);
            else
                addChild(badge);

            fac_addr = get_param_addr("facilitator", DEFAULT_FACILITATOR_ADDR);
            if (!fac_addr) {
                puts("Error: Facilitator spec must be in the form \"host:port\".");
                return;
            }

            if (this.loaderInfo.parameters["client"])
                client_main();
            else
                proxy_main();
        }

        /* Get an address structure from the given movie parameter, or the given
           default. Returns null on error. */
        private function get_param_addr(param:String, default_addr:Object):Object
        {
            var spec:String, addr:Object;

            spec = this.loaderInfo.parameters[param];
            if (spec)
                return parse_addr_spec(spec);
            else
                return default_addr;
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function proxy_main():void
        {
            var fac_url:String;
            var loader:URLLoader;

            if (num_proxy_pairs >= MAX_NUM_PROXY_PAIRS) {
                setTimeout(proxy_main, FACILITATOR_POLL_INTERVAL);
                return;
            }

            loader = new URLLoader();
            /* Get the x-www-form-urlencoded values. */
            loader.dataFormat = URLLoaderDataFormat.VARIABLES;
            loader.addEventListener(Event.COMPLETE, fac_complete);
            loader.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
            });
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
            });

            fac_url = "http://" + encodeURIComponent(fac_addr.host)
                + ":" + encodeURIComponent(fac_addr.port) + "/";
            puts("Facilitator: connecting to " + fac_url + ".");
            loader.load(new URLRequest(fac_url));
        }

        private function fac_complete(e:Event):void
        {
            var loader:URLLoader;
            var client_spec:String;
            var relay_spec:String;
            var proxy_pair:Object;

            setTimeout(proxy_main, FACILITATOR_POLL_INTERVAL);

            loader = e.target as URLLoader;
            client_spec = loader.data.client;
            if (client_spec == "") {
                puts("No clients.");
                return;
            } else if (!client_spec) {
                puts("Error: missing \"client\" in response.");
                return;
            }
            relay_spec = loader.data.relay;
            if (!relay_spec) {
                puts("Error: missing \"relay\" in response.");
                return;
            }
            puts("Facilitator: got client:\"" + client_spec + "\" "
                + "relay:\"" + relay_spec + "\".");

            try {
                proxy_pair = make_proxy_pair(client_spec, relay_spec);
            } catch (e:ArgumentError) {
                puts("Error: " + e);
                return;
            }
            proxy_pair.addEventListener(Event.COMPLETE, function(e:Event):void {
                proxy_pair.log("Complete.");
                num_proxy_pairs--;
                badge.proxy_end();
            });
            proxy_pair.connect();

            num_proxy_pairs++;
            badge.proxy_begin();
        }

        private function client_main():void
        {
            var rs:RTMFPSocket;

            rs = new RTMFPSocket(RTMFP_URL);
            rs.addEventListener(Event.COMPLETE, function (e:Event):void {
                puts("Got RTMFP id " + rs.id);
                register(rs);
            });
            rs.addEventListener(RTMFPSocket.ACCEPT_EVENT, client_accept);

            rs.listen();
        }

        private function client_accept(e:Event):void {
            var rs:RTMFPSocket;
            var s_t:Socket;
            var proxy_pair:ProxyPair;

            rs = e.target as RTMFPSocket;
            s_t = new Socket();

            puts("Got RTMFP connection from " + rs.peer_id);

            proxy_pair = new ProxyPair(this, rs, function ():void {
                /* Do nothing; already connected. */
            }, s_t, function ():void {
                s_t.connect(LOCAL_TOR_CLIENT_ADDR.host, LOCAL_TOR_CLIENT_ADDR.port);
            });
            proxy_pair.connect();
        }

        private function register(rs:RTMFPSocket):void {
            var fac_url:String;
            var loader:URLLoader;
            var request:URLRequest;
            var variables:URLVariables;

            loader = new URLLoader();
            loader.addEventListener(Event.COMPLETE, function (e:Event):void {
                puts("Facilitator: registered.");
            });
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
                rs.close();
            });
            loader.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
                rs.close();
            });

            fac_url = "http://" + encodeURIComponent(fac_addr.host)
                + ":" + encodeURIComponent(fac_addr.port) + "/";
            request = new URLRequest(fac_url);
            request.method = URLRequestMethod.POST;
            request.data = new URLVariables;
            request.data["client"] = rs.id;

            puts("Facilitator: connecting to " + fac_url + ".");
            loader.load(request);
        }

        private function make_proxy_pair(client_spec:String, relay_spec:String):ProxyPair
        {
            var proxy_pair:ProxyPair;
            var addr_c:Object;
            var addr_r:Object;
            var s_c:*;
            var s_r:Socket;

            addr_r = swfcat.parse_addr_spec(relay_spec);
            if (!addr_r)
                throw new ArgumentError("Relay spec must be in the form \"host:port\".");

            addr_c = swfcat.parse_addr_spec(client_spec);
            if (addr_c) {
                s_c = new Socket();
                s_r = new Socket();
                proxy_pair = new ProxyPair(this, s_c, function ():void {
                    s_c.connect(addr_c.host, addr_c.port);
                }, s_r, function ():void {
                    s_r.connect(addr_r.host, addr_r.port);
                });
                proxy_pair.set_name("<" + addr_c.host + ":" + addr_c.port + ","
                    + addr_r.host + ":" + addr_r.port + ">");
                return proxy_pair;
            }

            if (client_spec.match(/^[0-9A-Fa-f]{64}$/)) {
                s_c = new RTMFPSocket(RTMFP_URL);
                s_r = new Socket();
                proxy_pair = new ProxyPair(this, s_c, function ():void {
                    s_c.connect(client_spec);
                }, s_r, function ():void {
                    s_r.connect(addr_r.host, addr_r.port);
                });
                proxy_pair.set_name("<" + client_spec.substr(0, 4) + "...,"
                    + addr_r.host + ":" + addr_r.port + ">");
                return proxy_pair;
            }

            throw new ArgumentError("Can't parse client spec \"" + client_spec + "\".");
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private static function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.text.TextFormat;
import flash.text.TextField;
import flash.utils.getTimer;

class Badge extends flash.display.Sprite
{
    /* Number of proxy pairs currently connected. */
    private var num_proxy_pairs:int = 0;
    /* Number of proxy pairs ever connected. */
    private var total_proxy_pairs:int = 0;

    [Embed(source="badge.png")]
    private var BadgeImage:Class;
    private var tot_client_count_tf:TextField;
    private var tot_client_count_fmt:TextFormat;
    private var cur_client_count_tf:TextField;
    private var cur_client_count_fmt:TextFormat;

    public function Badge()
    {
        /* Setup client counter for badge. */
        tot_client_count_fmt = new TextFormat();
        tot_client_count_fmt.color = 0xFFFFFF;
        tot_client_count_fmt.align = "center";
        tot_client_count_fmt.font = "courier-new";
        tot_client_count_fmt.bold = true;
        tot_client_count_fmt.size = 10;
        tot_client_count_tf = new TextField();
        tot_client_count_tf.width = 20;
        tot_client_count_tf.height = 17;
        tot_client_count_tf.background = false;
        tot_client_count_tf.defaultTextFormat = tot_client_count_fmt;
        tot_client_count_tf.x=47;
        tot_client_count_tf.y=0;

        cur_client_count_fmt = new TextFormat();
        cur_client_count_fmt.color = 0xFFFFFF;
        cur_client_count_fmt.align = "center";
        cur_client_count_fmt.font = "courier-new";
        cur_client_count_fmt.bold = true;
        cur_client_count_fmt.size = 10;
        cur_client_count_tf = new TextField();
        cur_client_count_tf.width = 20;
        cur_client_count_tf.height = 17;
        cur_client_count_tf.background = false;
        cur_client_count_tf.defaultTextFormat = cur_client_count_fmt;
        cur_client_count_tf.x=47;
        cur_client_count_tf.y=6;

        addChild(new BadgeImage());
        addChild(tot_client_count_tf);
        addChild(cur_client_count_tf);

        /* Update the client counter on badge. */
        update_client_count();
    }

    public function proxy_begin():void
    {
        num_proxy_pairs++;
        total_proxy_pairs++;
        update_client_count();
    }

    public function proxy_end():void
    {
        num_proxy_pairs--;
        update_client_count();
    }

    private function update_client_count():void
    {
        /* Update total client count. */
        if (String(total_proxy_pairs).length == 1)
            tot_client_count_tf.text = "0" + String(total_proxy_pairs);
        else
            tot_client_count_tf.text = String(total_proxy_pairs);

        /* Update current client count. */
        cur_client_count_tf.text = "";
        for(var i:Number = 0; i < num_proxy_pairs; i++)
            cur_client_count_tf.appendText(".");
    }
}

class RateLimit
{
    public function RateLimit()
    {
    }

    public function update(n:Number):Boolean
    {
        return true;
    }

    public function when():Number
    {
        return 0.0;
    }

    public function is_limited():Boolean
    {
        return false;
    }
}

class RateUnlimit extends RateLimit
{
    public function RateUnlimit()
    {
    }

    public override function update(n:Number):Boolean
    {
        return true;
    }

    public override function when():Number
    {
        return 0.0;
    }

    public override function is_limited():Boolean
    {
        return false;
    }
}

class BucketRateLimit extends RateLimit
{
    private var amount:Number;
    private var capacity:Number;
    private var time:Number;
    private var last_update:uint;

    public function BucketRateLimit(capacity:Number, time:Number)
    {
        this.amount = 0.0;
        /* capacity / time is the rate we are aiming for. */
        this.capacity = capacity;
        this.time = time;
        this.last_update = getTimer();
    }

    private function age():void
    {
        var now:uint;
        var delta:Number;

        now = getTimer();
        delta = (now - last_update) / 1000.0;
        last_update = now;

        amount -= delta * capacity / time;
        if (amount < 0.0)
            amount = 0.0;
    }

    public override function update(n:Number):Boolean
    {
        age();
        amount += n;

        return amount <= capacity;
    }

    public override function when():Number
    {
        age();
        return (amount - capacity) / (capacity / time);
    }

    public override function is_limited():Boolean
    {
        age();
        return amount > capacity;
    }
}

Rename CIRRUS_URL to RTMFP_URL.

Rename CIRRUS_URL to RTMFP_URL.


/*
    Weave (Web-based Analysis and Visualization Environment)
    Copyright (C) 2008-2011 University of Massachusetts Lowell

    This file is a part of Weave.

    Weave is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License, Version 3,
    as published by the Free Software Foundation.

    Weave is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Weave.  If not, see <http://www.gnu.org/licenses/>.
*/

package weave.data
{
	import flash.utils.ByteArray;
	import flash.utils.getTimer;

	import mx.utils.ObjectUtil;
	import mx.utils.StringUtil;

	import weave.api.WeaveAPI;
	import weave.api.core.ILinkableObject;
	import weave.api.data.ICSVParser;
	import weave.api.getCallbackCollection;
	import weave.utils.AsyncSort;

	/**
	 * This is an all-static class containing functions to parse and generate valid CSV files.
	 * Ported from AutoIt Script to Flex. Original author: adufilie
	 *
	 * @author skolman
	 * @author adufilie
	 */
	public class CSVParser implements ICSVParser, ILinkableObject
	{
		private static const CR:String = '\r';
		private static const LF:String = '\n';
		private static const CRLF:String = '\r\n';

		/**
		 * @param delimiter
		 * @param quote
		 * @param asyncMode If this is set to true, parseCSV() will work asynchronously and trigger callbacks when it finishes.
		 *                  Note that if asyncMode is enabled, you can only parse one CSV string at a time.
		 */
		public function CSVParser(asyncMode:Boolean = false, delimiter:String = ',', quote:String = '"')
		{
			this.asyncMode = asyncMode;
			if (delimiter && delimiter.length == 1)
				this.delimiter = delimiter;
			if (quote && quote.length == 1)
				this.quote = quote;
		}

		// modes set in constructor
		private var asyncMode:Boolean;
		private var delimiter:String = ',';
		private var quote:String = '"';

		// async state
		private var csvData:String;
		private var csvDataArray:Array;
		private var parseTokens:Boolean;
		private var i:int;
		private var row:int;
		private var col:int;
		private var escaped:Boolean;

		/**
		 * @return  The resulting two-dimensional Array from the last call to parseCSV().
		 */
		public function get parseResult():Array
		{
			return csvDataArray;
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSV(csvData:String, parseTokens:Boolean = true):Array
		{
			// initialization
			this.csvData = csvData;
			this.csvDataArray = [];
			this.parseTokens = parseTokens;
			this.i = 0;
			this.row = 0;
			this.col = 0;
			this.escaped = false;

			if (asyncMode)
			{
				WeaveAPI.StageUtils.startTask(this, parseIterate, WeaveAPI.TASK_PRIORITY_3_PARSING, parseDone);
			}
			else
			{
				parseIterate(int.MAX_VALUE);
				parseDone();
			}

			return csvDataArray;
		}

		private function parseIterate(stopTime:int):Number
		{
			// run initialization code on first iteration
			if (i == 0)
			{
				if (!csvData) // null or empty string?
					return 1; // done

				// start off first row with an empty string token
				csvDataArray[row] = [''];
			}

			while (getTimer() < stopTime)
			{
				if (i >= csvData.length)
					return 1; // done

				var currentChar:String = csvData.charAt(i);
				var twoChar:String = currentChar + csvData.charAt(i+1);
				if (escaped)
				{
					if (twoChar == quote+quote) //escaped quote
					{
						csvDataArray[row][col] += (parseTokens?currentChar:twoChar);//append quote(s) to current token
						i += 1; //skip second quote mark
					}
					else if (currentChar == quote)	//end of escaped text
					{
						escaped = false;
						if (!parseTokens)
						{
							csvDataArray[row][col] += currentChar;//append quote to current token
						}
					}
					else
					{
						csvDataArray[row][col] += currentChar;//append quotes to current token
					}
				}
				else
				{

					if (twoChar == delimiter+quote)
					{
						escaped = true;
						col += 1;
						csvDataArray[row][col] = (parseTokens?'':quote);
						i += 1; //skip quote mark
					}
					else if (currentChar == quote && csvDataArray[row][col] == '')		//start new token
					{
						escaped = true;
						if (!parseTokens)
							csvDataArray[row][col] += currentChar;
					}
					else if (currentChar == delimiter)		//start new token
					{
						col += 1;
						csvDataArray[row][col] = '';
					}
					else if (twoChar == CRLF)	//then start new row
					{
						i += 1; //skip line feed
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else if (currentChar == CR)	//then start new row
					{
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else if (currentChar == LF)	//then start new row
					{
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else //append single character to current token
						csvDataArray[row][col] += currentChar;
				}
				i++;
			}

			return i / csvData.length;
		}

		private function parseDone():void
		{
			// if there is more than one row and last row is empty,
			// remove last row assuming it is there because of a newline at the end of the file.
			for (var iRow:int = csvDataArray.length; iRow--;)
			{
				var dataLine:Array = csvDataArray[iRow];

				if (dataLine.length == 1 && dataLine[0] == '')
					csvDataArray.splice(iRow, 1);
			}

			if (asyncMode)
				getCallbackCollection(this).triggerCallbacks();
		}

		/**
		 * @inheritDoc
		 */
		public function createCSV(rows:Array):String
		{
			var firstRow:Boolean = true;
			var out:ByteArray = new ByteArray();
			for each (var row:Array in rows)
			{
				if (firstRow)
					firstRow = false;
				else
					out.writeUTFBytes(LF);

				var firstItem:Boolean = true;
				for each (var item:String in row)
				{
					if (firstItem)
						firstItem = false;
					else
						out.writeUTFBytes(delimiter);

					out.writeUTFBytes(createCSVToken(item));
				}
			}
			out.position = 0;
			return out.readUTFBytes(out.length);
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSVRow(csvData:String, parseTokens:Boolean = true):Array
		{
			if (csvData == null)
				return null;

			var rows:Array = parseCSV(csvData, parseTokens);
			if (rows.length == 0)
				return rows;
			if (rows.length == 1)
				return rows[0];
			// flatten
			return [].concat.apply(null, rows);
		}

		/**
		 * @inheritDoc
		 */
		public function createCSVRow(row:Array):String
		{
			return createCSV([row]);
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSVToken(token:String):String
		{
			var parsedToken:String = '';

			var tokenLength:int = token.length;

			if (token.charAt(0) == quote)
			{
				var escaped:Boolean = true;
				for (var i:int = 1; i <= tokenLength; i++)
				{
					var currentChar:String = token.charAt(i);
					var twoChar:String = currentChar + token.charAt(i+1);

					if (twoChar == quote+quote) //append escaped quote
					{
						i += 1;
						parsedToken += quote;
					}
					else if (currentChar == quote && escaped)
					{
						escaped = false;
					}
					else
					{
						parsedToken += currentChar;
					}
				}
			}
			else
			{
				parsedToken = token;
			}
			return parsedToken;
		}

		/**
		 * @inheritDoc
		 */
		public function createCSVToken(str:String):String
		{
			if (str == null)
				str = '';

			// determine if quotes are necessary
			if ( str.length > 0
				&& str.indexOf(quote) < 0
				&& str.indexOf(delimiter) < 0
				&& str.indexOf(LF) < 0
				&& str.indexOf(CR) < 0
				&& str == StringUtil.trim(str) )
			{
				return str;
			}

			var token:String = quote;
			for (var i:int = 0; i <= str.length; i++)
			{
				var currentChar:String = str.charAt(i);
				if (currentChar == quote)
					token += quote + quote;
				else
					token += currentChar;
			}
			return token + quote;
		}

		/**
		 * @inheritDoc
		 */
		public function convertRowsToRecords(rows:Array, headerDepth:int = 1):Array
		{
			if (rows.length < headerDepth)
				throw new Error("headerDepth is greater than the number of rows");
			assertHeaderDepth(headerDepth);

			var records:Array = new Array(rows.length - headerDepth);
			for (var r:int = headerDepth; r < rows.length; r++)
			{
				var record:Object = {};
				var row:Array = rows[r];
				for (var c:int = 0; c < row.length; c++)
				{
					var output:Object = record;
					var cell:Object = row[c];
					for (var h:int = 0; h < headerDepth; h++)
					{
						var colName:String = rows[h][c];
						if (h < headerDepth - 1)
						{
							if (!output[colName])
								output[colName] = {};
							output = output[colName];
						}
						else
							output[colName] = cell;
					}
				}
				records[r - headerDepth] = record;
			}
			return records;
		}

		/**
		 * @inheritDoc
		 */
		public function getRecordFieldNames(records:Array, includeNullFields:Boolean = false, headerDepth:int = 1):Array
		{
			assertHeaderDepth(headerDepth);

			var nestedFieldNames:Object = {};
			for each (var record:Object in records)
				_outputNestedFieldNames(record, includeNullFields, nestedFieldNames, headerDepth);

			var fields:Array = [];
			_collapseNestedFieldNames(nestedFieldNames, fields);
			return fields;
		}
		private function _outputNestedFieldNames(record:Object, includeNullFields:Boolean, output:Object, depth:int):void
		{
			for (var field:String in record)
			{
				if (includeNullFields || record[field] != null)
				{
					if (depth == 1)
					{
						output[field] = false;
					}
					else
					{
						if (!output[field])
							output[field] = {};
						_outputNestedFieldNames(record[field], includeNullFields, output[field], depth - 1);
					}
				}
			}
		}
		private function _collapseNestedFieldNames(nestedFieldNames:Object, output:Array, prefix:Array = null):void
		{
			for (var field:String in nestedFieldNames)
			{
				if (nestedFieldNames[field]) // either an Object or false
				{
					_collapseNestedFieldNames(nestedFieldNames[field], output, prefix ? prefix.concat(field) : [field]);
				}
				else // false means reached full nesting depth
				{
					if (prefix) // is depth > 1?
						output.push(prefix.concat(field)); // output the list of nested field names
					else
						output.push(field); // no array when max depth is 1
				}
			}
		}

		/**
		 * @inheritDoc
		 */
		public function convertRecordsToRows(records:Array, columnOrder:Array = null, allowBlankColumns:Boolean = false, headerDepth:int = 1):Array
		{
			assertHeaderDepth(headerDepth);

			var fields:Array = columnOrder;
			if (fields == null)
			{
				fields = getRecordFieldNames(records, allowBlankColumns, headerDepth);
				AsyncSort.sortImmediately(fields);
			}

			var r:int;
			var c:int;
			var cell:Object;
			var row:Array;
			var rows:Array = new Array(records.length + headerDepth);

			// construct multiple header rows from field name chains
			for (r = 0; r < headerDepth; r++)
			{
				row = new Array(fields.length);
				for (c = 0; c < fields.length; c++)
				{
					if (headerDepth > 1)
						row[c] = fields[c][r] || ''; // fields are Arrays
					else
						row[c] = fields[c] || ''; // fields are Strings
				}
				rows[r] = row;
			}

			for (r = 0; r < records.length; r++)
			{
				var record:Object = records[r];
				row = new Array(fields.length);
				for (c = 0; c < fields.length; c++)
				{
					if (headerDepth == 1)
					{
						// fields is an Array of Strings
						cell = record[fields[c]];
					}
					else
					{
						// fields is an Array of Arrays
						cell = record;
						for each (var field:String in fields[c])
							if (cell)
								cell = cell[field];
					}
					row[c] = cell != null ? String(cell) : '';
				}
				rows[headerDepth + r] = row;
			}
			return rows;
		}

		private static function assertHeaderDepth(headerDepth:int):void
		{
			if (headerDepth < 1)
				throw new Error("headerDepth must be > 0");
		}

		//test();
		private static var _tested:Boolean = false;
		private static function test():void
		{
			if (_tested)
				return;
			_tested = true;

			var _:Object = {};
			_.parser = WeaveAPI.CSVParser;
			_.csv=[
				"internal,internal,public,public,public,private,private,test",
				"id,type,title,keyType,dataType,connection,sqlQuery,empty",
				"2,1,state name,fips,string,resd,\"select fips,name from myschema.state_data\",",
				"3,1,population,fips,number,resd,\"select fips,pop from myschema.state_data\",",
				"1,0,state data table"
			].join('\n');
			_.table = _.parser.parseCSV(_.csv);
			_.records = _.parser.convertRowsToRecords(_.table, 2);
			_.rows = _.parser.convertRecordsToRows(_.records, null, false, 2);
			_.fields = _.parser.getRecordFieldNames(_.records, false, 2);
			_.fieldOrder = _.parser.parseCSV('internal,id\ninternal,type\npublic,title\npublic,keyType\npublic,dataType\nprivate,connection\nprivate,sqlQuery');
			_.rows2 = _.parser.convertRecordsToRows(_.records, _.fieldOrder, false, 2);
			weaveTrace(ObjectUtil.toString(_));
		}
	}
}

/*
    Weave (Web-based Analysis and Visualization Environment)
    Copyright (C) 2008-2011 University of Massachusetts Lowell

    This file is a part of Weave.

    Weave is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License, Version 3,
    as published by the Free Software Foundation.

    Weave is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Weave.  If not, see <http://www.gnu.org/licenses/>.
*/

package weave.data
{
	import flash.utils.getTimer;

	import mx.utils.ObjectUtil;
	import mx.utils.StringUtil;

	import weave.api.WeaveAPI;
	import weave.api.core.ILinkableObject;
	import weave.api.data.ICSVParser;
	import weave.api.getCallbackCollection;
	import weave.utils.AsyncSort;

	/**
	 * This is an all-static class containing functions to parse and generate valid CSV files.
	 * Ported from AutoIt Script to Flex. Original author: adufilie
	 *
	 * @author skolman
	 * @author adufilie
	 */
	public class CSVParser implements ICSVParser, ILinkableObject
	{
		private static const CR:String = '\r';
		private static const LF:String = '\n';
		private static const CRLF:String = '\r\n';

		/**
		 * @param delimiter
		 * @param quote
		 * @param asyncMode If this is set to true, parseCSV() will work asynchronously and trigger callbacks when it finishes.
		 *                  Note that if asyncMode is enabled, you can only parse one CSV string at a time.
		 */
		public function CSVParser(asyncMode:Boolean = false, delimiter:String = ',', quote:String = '"')
		{
			this.asyncMode = asyncMode;
			if (delimiter && delimiter.length == 1)
				this.delimiter = delimiter;
			if (quote && quote.length == 1)
				this.quote = quote;
		}

		// modes set in constructor
		private var asyncMode:Boolean;
		private var delimiter:String = ',';
		private var quote:String = '"';

		// async state
		private var csvData:String;
		private var csvDataArray:Array;
		private var parseTokens:Boolean;
		private var i:int;
		private var row:int;
		private var col:int;
		private var escaped:Boolean;

		/**
		 * @return  The resulting two-dimensional Array from the last call to parseCSV().
		 */
		public function get parseResult():Array
		{
			return csvDataArray;
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSV(csvData:String, parseTokens:Boolean = true):Array
		{
			// initialization
			this.csvData = csvData;
			this.csvDataArray = [];
			this.parseTokens = parseTokens;
			this.i = 0;
			this.row = 0;
			this.col = 0;
			this.escaped = false;

			if (asyncMode)
			{
				WeaveAPI.StageUtils.startTask(this, parseIterate, WeaveAPI.TASK_PRIORITY_3_PARSING, parseDone);
			}
			else
			{
				parseIterate(int.MAX_VALUE);
				parseDone();
			}

			return csvDataArray;
		}

		private function parseIterate(stopTime:int):Number
		{
			// run initialization code on first iteration
			if (i == 0)
			{
				if (!csvData) // null or empty string?
					return 1; // done

				// start off first row with an empty string token
				csvDataArray[row] = [''];
			}

			while (getTimer() < stopTime)
			{
				if (i >= csvData.length)
					return 1; // done

				var currentChar:String = csvData.charAt(i);
				var twoChar:String = currentChar + csvData.charAt(i+1);
				if (escaped)
				{
					if (twoChar == quote+quote) //escaped quote
					{
						csvDataArray[row][col] += (parseTokens?currentChar:twoChar);//append quote(s) to current token
						i += 1; //skip second quote mark
					}
					else if (currentChar == quote)	//end of escaped text
					{
						escaped = false;
						if (!parseTokens)
						{
							csvDataArray[row][col] += currentChar;//append quote to current token
						}
					}
					else
					{
						csvDataArray[row][col] += currentChar;//append quotes to current token
					}
				}
				else
				{

					if (twoChar == delimiter+quote)
					{
						escaped = true;
						col += 1;
						csvDataArray[row][col] = (parseTokens?'':quote);
						i += 1; //skip quote mark
					}
					else if (currentChar == quote && csvDataArray[row][col] == '')		//start new token
					{
						escaped = true;
						if (!parseTokens)
							csvDataArray[row][col] += currentChar;
					}
					else if (currentChar == delimiter)		//start new token
					{
						col += 1;
						csvDataArray[row][col] = '';
					}
					else if (twoChar == CRLF)	//then start new row
					{
						i += 1; //skip line feed
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else if (currentChar == CR)	//then start new row
					{
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else if (currentChar == LF)	//then start new row
					{
						row += 1;
						col = 0;
						csvDataArray[row] = [''];
					}
					else //append single character to current token
						csvDataArray[row][col] += currentChar;
				}
				i++;
			}

			return i / csvData.length;
		}

		private function parseDone():void
		{
			// if there is more than one row and last row is empty,
			// remove last row assuming it is there because of a newline at the end of the file.
			for (var iRow:int = csvDataArray.length; iRow--;)
			{
				var dataLine:Array = csvDataArray[iRow];

				if (dataLine.length == 1 && dataLine[0] == '')
					csvDataArray.splice(iRow, 1);
			}

			if (asyncMode)
				getCallbackCollection(this).triggerCallbacks();
		}

		/**
		 * @inheritDoc
		 */
		public function createCSV(rows:Array):String
		{
			var lines:Array = new Array(rows.length);
			for (var i:int = rows.length; i--;)
			{
				var tokens:Array = new Array(rows[i].length);
				for (var j:int = tokens.length; j--;)
					tokens[j] = createCSVToken(rows[i][j]);

				lines[i] = tokens.join(delimiter);
			}
			var csvData:String = lines.join(LF);
			return csvData;
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSVRow(csvData:String, parseTokens:Boolean = true):Array
		{
			if (csvData == null)
				return null;

			var rows:Array = parseCSV(csvData, parseTokens);
			if (rows.length == 0)
				return rows;
			if (rows.length == 1)
				return rows[0];
			// flatten
			return [].concat.apply(null, rows);
		}

		/**
		 * @inheritDoc
		 */
		public function createCSVRow(row:Array):String
		{
			return createCSV([row]);
		}

		/**
		 * @inheritDoc
		 */
		public function parseCSVToken(token:String):String
		{
			var parsedToken:String = '';

			var tokenLength:int = token.length;

			if (token.charAt(0) == quote)
			{
				var escaped:Boolean = true;
				for (var i:int = 1; i <= tokenLength; i++)
				{
					var currentChar:String = token.charAt(i);
					var twoChar:String = currentChar + token.charAt(i+1);

					if (twoChar == quote+quote) //append escaped quote
					{
						i += 1;
						parsedToken += quote;
					}
					else if (currentChar == quote && escaped)
					{
						escaped = false;
					}
					else
					{
						parsedToken += currentChar;
					}
				}
			}
			else
			{
				parsedToken = token;
			}
			return parsedToken;
		}

		/**
		 * @inheritDoc
		 */
		public function createCSVToken(str:String):String
		{
			if (str == null)
				str = '';

			// determine if quotes are necessary
			if ( str.length > 0
				&& str.indexOf(quote) < 0
				&& str.indexOf(delimiter) < 0
				&& str.indexOf(LF) < 0
				&& str.indexOf(CR) < 0
				&& str == StringUtil.trim(str) )
			{
				return str;
			}

			var token:String = quote;
			for (var i:int = 0; i <= str.length; i++)
			{
				var currentChar:String = str.charAt(i);
				if (currentChar == quote)
					token += quote + quote;
				else
					token += currentChar;
			}
			return token + quote;
		}

		/**
		 * @inheritDoc
		 */
		public function convertRowsToRecords(rows:Array, headerDepth:int = 1):Array
		{
			if (rows.length < headerDepth)
				throw new Error("headerDepth is greater than the number of rows");
			assertHeaderDepth(headerDepth);

			var records:Array = new Array(rows.length - headerDepth);
			for (var r:int = headerDepth; r < rows.length; r++)
			{
				var record:Object = {};
				var row:Array = rows[r];
				for (var c:int = 0; c < row.length; c++)
				{
					var output:Object = record;
					var cell:Object = row[c];
					for (var h:int = 0; h < headerDepth; h++)
					{
						var colName:String = rows[h][c];
						if (h < headerDepth - 1)
						{
							if (!output[colName])
								output[colName] = {};
							output = output[colName];
						}
						else
							output[colName] = cell;
					}
				}
				records[r - headerDepth] = record;
			}
			return records;
		}

		/**
		 * @inheritDoc
		 */
		public function getRecordFieldNames(records:Array, includeNullFields:Boolean = false, headerDepth:int = 1):Array
		{
			assertHeaderDepth(headerDepth);

			var nestedFieldNames:Object = {};
			for each (var record:Object in records)
				_outputNestedFieldNames(record, includeNullFields, nestedFieldNames, headerDepth);

			var fields:Array = [];
			_collapseNestedFieldNames(nestedFieldNames, fields);
			return fields;
		}
		private function _outputNestedFieldNames(record:Object, includeNullFields:Boolean, output:Object, depth:int):void
		{
			for (var field:String in record)
			{
				if (includeNullFields || record[field] != null)
				{
					if (depth == 1)
					{
						output[field] = false;
					}
					else
					{
						if (!output[field])
							output[field] = {};
						_outputNestedFieldNames(record[field], includeNullFields, output[field], depth - 1);
					}
				}
			}
		}
		private function _collapseNestedFieldNames(nestedFieldNames:Object, output:Array, prefix:Array = null):void
		{
			for (var field:String in nestedFieldNames)
			{
				if (nestedFieldNames[field]) // either an Object or false
				{
					_collapseNestedFieldNames(nestedFieldNames[field], output, prefix ? prefix.concat(field) : [field]);
				}
				else // false means reached full nesting depth
				{
					if (prefix) // is depth > 1?
						output.push(prefix.concat(field)); // output the list of nested field names
					else
						output.push(field); // no array when max depth is 1
				}
			}
		}

		/**
		 * @inheritDoc
		 */
		public function convertRecordsToRows(records:Array, columnOrder:Array = null, allowBlankColumns:Boolean = false, headerDepth:int = 1):Array
		{
			assertHeaderDepth(headerDepth);

			var fields:Array = columnOrder;
			if (fields == null)
			{
				fields = getRecordFieldNames(records, allowBlankColumns, headerDepth);
				AsyncSort.sortImmediately(fields);
			}

			var r:int;
			var c:int;
			var cell:Object;
			var row:Array;
			var rows:Array = new Array(records.length + headerDepth);

			// construct multiple header rows from field name chains
			for (r = 0; r < headerDepth; r++)
			{
				row = new Array(fields.length);
				for (c = 0; c < fields.length; c++)
				{
					if (headerDepth > 1)
						row[c] = fields[c][r] || ''; // fields are Arrays
					else
						row[c] = fields[c] || ''; // fields are Strings
				}
				rows[r] = row;
			}

			for (r = 0; r < records.length; r++)
			{
				var record:Object = records[r];
				row = new Array(fields.length);
				for (c = 0; c < fields.length; c++)
				{
					if (headerDepth == 1)
					{
						// fields is an Array of Strings
						cell = record[fields[c]];
					}
					else
					{
						// fields is an Array of Arrays
						cell = record;
						for each (var field:String in fields[c])
							if (cell)
								cell = cell[field];
					}
					row[c] = cell != null ? String(cell) : '';
				}
				rows[headerDepth + r] = row;
			}
			return rows;
		}

		private static function assertHeaderDepth(headerDepth:int):void
		{
			if (headerDepth < 1)
				throw new Error("headerDepth must be > 0");
		}

		//test();
		private static var _tested:Boolean = false;
		private static function test():void
		{
			if (_tested)
				return;
			_tested = true;

			var _:Object = {};
			_.parser = WeaveAPI.CSVParser;
			_.csv=[
				"internal,internal,public,public,public,private,private,test",
				"id,type,title,keyType,dataType,connection,sqlQuery,empty",
				"2,1,state name,fips,string,resd,\"select fips,name from myschema.state_data\",",
				"3,1,population,fips,number,resd,\"select fips,pop from myschema.state_data\",",
				"1,0,state data table"
			].join('\n');
			_.table = _.parser.parseCSV(_.csv);
			_.records = _.parser.convertRowsToRecords(_.table, 2);
			_.rows = _.parser.convertRecordsToRows(_.records, null, false, 2);
			_.fields = _.parser.getRecordFieldNames(_.records, false, 2);
			_.fieldOrder = _.parser.parseCSV('internal,id\ninternal,type\npublic,title\npublic,keyType\npublic,dataType\nprivate,connection\nprivate,sqlQuery');
			_.rows2 = _.parser.convertRecordsToRows(_.records, _.fieldOrder, false, 2);
			weaveTrace(ObjectUtil.toString(_));
		}
	}
}

Revert "Now using ByteArray in CSVParser.createCSV() for 2x speedup."

This reverts commit 721aaba8b1b70953a764dabf3dc743372a206d84.


// vim: tabstop=4 shiftwidth=4

// Copyright (c) 2010 , 杨博 (Yang Bo) All rights reserved.
//
//         pop.atry@gmail.com
//
// Use, modification and distribution are subject to the "New BSD License"
// as listed at <url: http://www.opensource.org/licenses/bsd-license.php >.

package com.netease.protobuf {
	import flash.utils.*;
	public function messageToString(message:Object):String {
		var s:String = getQualifiedClassName(message) + "(\n"
		const descriptor:XML = describeType(message)
		for each(var getter:String in descriptor.accessor.(@access != "writeonly").@name) {
			if (getter.search(/^has(.)(.*)$/) != -1) {
				continue
			}
			s += fieldToString(message, descriptor, getter)
		}
		for each(var field:String in descriptor.variable.@name) {
			s += fieldToString(message, descriptor, field)
		}
		for (var k:* in message) {
			s += k + "=" + message[k] + ";\n"
		}
		s += ")"
		return s
	}
}

function fieldToString(message:Object, descriptor:XML, name:String):String {
	var hasField:String = "has" + name.charAt(0).toUpperCase() + name.substr(1)
	if (descriptor.accessor.(@name == hasField).length() != 0 && !message[hasField]) {
		return ""
	}
	var field:* = message[name]
	if (field.constructor == Array && field.length == 0) {
		return ""
	}
	return name + "=" + message[name] + ";\n"
}

// vim: tabstop=4 shiftwidth=4

// Copyright (c) 2010 , 杨博 (Yang Bo) All rights reserved.
//
//         pop.atry@gmail.com
//
// Use, modification and distribution are subject to the "New BSD License"
// as listed at <url: http://www.opensource.org/licenses/bsd-license.php >.

package com.netease.protobuf {
	import flash.utils.*;
	public function messageToString(message:Object):String {
		var s:String = getQualifiedClassName(message) + "(\n"
		const descriptor:XML = describeType(message)
		for each(var getter:String in descriptor.accessor.(@access != "writeonly").@name) {
			if (getter.search(/^has(.)(.*)$/) != -1) {
				continue
			}
			s += fieldToString(message, descriptor, getter)
		}
		for each(var field:String in descriptor.variable.@name) {
			s += fieldToString(message, descriptor, field)
		}
		for (var k:* in message) {
			s += k + "=" + message[k] + ";\n"
		}
		s += ")"
		return s
	}
}

function fieldToString(message:Object, descriptor:XML, name:String):String {
	var hasField:String = "has" + name.charAt(0).toUpperCase() + name.substr(1)
	if (descriptor.accessor.(@name == hasField).length() != 0 && !message[hasField]) {
		return ""
	}
	var field:* = message[name]
	if (field != null && field.constructor == Array && field.length == 0) {
		return ""
	}
	return name + "=" + field + ";\n"
}

修复当某一项为 null 时，toString 出错的 bug

修复当某一项为 null 时，toString 出错的 bug


package lambmei.starling.display
{
	import flash.display.MovieClip;
	import flash.geom.Point;

	import starling.display.Button;
	import starling.display.DisplayObject;
	import starling.display.DisplayObjectContainer;
	import starling.display.Sprite;
	import starling.events.Event;
	import starling.events.Touch;
	import starling.events.TouchEvent;
	import starling.events.TouchPhase;
	import starling.textures.Texture;

	public class HandleSheet extends Sprite
	{

		/** 點擊 將物件移動到前方 **/
		protected var _touchBringToFront:Boolean
		public function get touchBringToFront():Boolean { return _touchBringToFront;}
		public function set touchBringToFront(value:Boolean):void{ _touchBringToFront = value; }

		/** 是否選擇 **/
		protected var _selected:Boolean
		public function get selected():Boolean { return _selected;}
		public function set selected(value:Boolean):void
		{
			if(value!=_selected){
				_selected = value;
				_selectedGroup.visible = _selected
			}
		}

		/** 是否有使用 控制按鈕 Read Only **/
		protected var _useCtrlButton:Boolean
		public function get useCtrlButton():Boolean
		{
			return _useCtrlButton;
		}

//		public function set useCtrlButton(value:Boolean):void
//		{
//			_useCtrlButton = value;
//		}

		//Const Vars
		protected static const CTRL_BUTTON_NAME:String = "HandlectrlBtn"
		public static const ALIGN_CENTER:String = "center"
		public static const ALIGN_LT:String = "LT"

		//UI
		protected var _contents:DisplayObject
		protected var _ctrlButton:DisplayObject
		protected var _selectedGroup:Sprite

		public function HandleSheet(contents:DisplayObject=null)
		{
			addEventListener(TouchEvent.TOUCH, onTouch);
			useHandCursor = true;

			_touchBringToFront = true

			if (contents)
			{
				var _w:Number = contents.width
				var _h:Number = contents.height
				var _halfW:Number = _w/2
				var _halfH:Number= _h/2
				//將物件置中
				contents.x = int(_halfW * -1);
				contents.y = int(_halfH * -1);
				addChild(contents);
				_contents = contents

			}
			//init SelectGroup
			_selectedGroup = new Sprite()
			this.addChild(this._selectedGroup);

			_selectedGroup.visible = _selected
		}

		public function setCtrlButtonInitByTexture(upTexture:Texture , downTexture:Texture = null ):void
		{
			_useCtrlButton = true

			if ( upTexture !=null)
			{
				_ctrlButton = new Button(upTexture,CTRL_BUTTON_NAME,downTexture);

				setCtrlButtonInitByObject(_ctrlButton )
			}else{
				throw new ArgumentError("Texture is Null!")
			}
		}

		/** 設定控制按鈕 by 物件 可以是 image sprite button **/
		public function setCtrlButtonInitByObject( obj:DisplayObject ):void
		{
			_useCtrlButton = true

			if (_contents)
			{
				_ctrlButton = obj

				var _w:Number = _contents.width
				var _h:Number = _contents.height
				var _halfW:Number = _w/2
				var _halfH:Number= _h/2
				//放置右上角
				_ctrlButton.x = int(_halfW)
				_ctrlButton.y = - int(_halfH)

				//置中對齊
				_ctrlButton.x -= int(_ctrlButton.width/2)
				_ctrlButton.y -= int(_ctrlButton.height/2)

				_selectedGroup.addChild(_ctrlButton)
			}
		}

		protected function onTouch(event:TouchEvent):void
		{
			var touches:Vector.<Touch> = event.getTouches(this, TouchPhase.MOVED);
			var touch:Touch

			touch = event.getTouch(this, TouchPhase.BEGAN);
			onTouchBegan(touch)

			//一隻手指
			if (touches.length == 1)
			{
				//檢查是否是Ctrl
				if(isTouchCtrlButton(event.target) )
				{
					resizeAndRotationByCtrlButton(touches)
				}
				else
				{
					//一隻手指平移
					var delta:Point = touches[0].getMovement(parent);

					x += delta.x;
					y += delta.y;
				}
			}
			else if (touches.length == 2)
			{
				//兩隻手指
				resizeAndRotationByTwoFingers(touches)
			}

			touch  = event.getTouch(this, TouchPhase.ENDED);
			onTouchEnd(touch)

		}

		/**當touch開始**/
		protected function onTouchBegan(touch:Touch):void
		{
			//touch 開始要做的事情
			if(touch){
				if(_touchBringToFront){
					parent.addChild(this);
				}
				selected = true
			}

		}

		/**當touch結束**/
		protected function onTouchEnd(touch:Touch):void
		{

		}

		/**檢查是否touch 控制按鈕**/
		protected function isTouchCtrlButton(target:*):Boolean
		{

			var _do		:DisplayObject
			var _doc	:DisplayObjectContainer

			if(_ctrlButton is  DisplayObjectContainer){
				_doc = _ctrlButton as DisplayObjectContainer
				return _doc.contains(target)
			}
			else if(_ctrlButton is DisplayObject)
			{
				_do = _ctrlButton as DisplayObject
				return _do == target
			}
			return false
		}

		/** 單隻手指使用控制按鈕 放大縮小旋轉 **/
		protected function resizeAndRotationByCtrlButton(touches:Vector.<Touch>):void
		{

			var touchA:Touch = touches[0];
			var touchB:Touch = touchA.clone();		//模擬B點
			var n = touchA.getLocation(this);
			//鏡射A點坐標
			touchB.globalX = n.x * 1
			touchB.globalY = n.y * -1

			var currentPosA:Point  = touchA.getLocation(this);
			var previousPosA:Point = touchA.getPreviousLocation(this);

			//鏡射A點
			var currentPosB:Point  = currentPosA.clone();
			currentPosB.x *=-1;
			currentPosB.y *=-1;
			//鏡射A點
			var previousPosB:Point  = previousPosA.clone();
			previousPosB.x *=-1;
			previousPosB.y *=-1;

			var currentVector:Point  = currentPosA.subtract(currentPosB);
			var previousVector:Point = previousPosA.subtract(previousPosB);

			var currentAngle:Number  = Math.atan2(currentVector.y, currentVector.x);
			var previousAngle:Number = Math.atan2(previousVector.y, previousVector.x);

			var deltaAngle:Number = currentAngle - previousAngle;

			// rotate
			rotation += deltaAngle;

			// scale
			var sizeDiff:Number = currentVector.length / previousVector.length;
			scaleX *= sizeDiff;
			scaleY *= sizeDiff;

			//_ctrlButton 保持原比例
			if(_ctrlButton){
				_ctrlButton.scaleX /= sizeDiff
				_ctrlButton.scaleY /= sizeDiff
			}

		}

		/** 使用兩隻手指 使用放大縮小旋轉 保留官方功能 **/
		protected function resizeAndRotationByTwoFingers(touches:Vector.<Touch>):void
		{
			//保留官方原本兩隻手指操作的功能
			// two fingers touching -> rotate and scale
			var touchA:Touch = touches[0];
			var touchB:Touch = touches[1];

			var currentPosA:Point  = touchA.getLocation(parent);
			var previousPosA:Point = touchA.getPreviousLocation(parent);
			var currentPosB:Point  = touchB.getLocation(parent);
			var previousPosB:Point = touchB.getPreviousLocation(parent);

			var currentVector:Point  = currentPosA.subtract(currentPosB);
			var previousVector:Point = previousPosA.subtract(previousPosB);

			var currentAngle:Number  = Math.atan2(currentVector.y, currentVector.x);
			var previousAngle:Number = Math.atan2(previousVector.y, previousVector.x);
			var deltaAngle:Number = currentAngle - previousAngle;

			// update pivot point based on previous center
			var previousLocalA:Point  = touchA.getPreviousLocation(this);
			var previousLocalB:Point  = touchB.getPreviousLocation(this);
			trace(previousLocalA , previousLocalB)

			pivotX = (previousLocalA.x + previousLocalB.x) * 0.5;
			pivotY = (previousLocalA.y + previousLocalB.y) * 0.5;

			// update location based on the current center
			x = (currentPosA.x + currentPosB.x) * 0.5;
			y = (currentPosA.y + currentPosB.y) * 0.5;

			// rotate
			rotation += deltaAngle;

			// scale
			var sizeDiff:Number = currentVector.length / previousVector.length;
			scaleX *= sizeDiff;
			scaleY *= sizeDiff;
		}

		public override function dispose():void
		{
			removeEventListener(TouchEvent.TOUCH, onTouch);
			if(_contents){
				_contents.dispose()
			}
			super.dispose();
		}
	}
}
import lambmei.starling.display.HandleSheet;

package lambmei.starling.display
{
	import flash.display.MovieClip;
	import flash.geom.Point;
	import flash.system.System;

	import starling.display.Button;
	import starling.display.DisplayObject;
	import starling.display.DisplayObjectContainer;
	import starling.display.Shape;
	import starling.display.Sprite;
	import starling.events.Event;
	import starling.events.Touch;
	import starling.events.TouchEvent;
	import starling.events.TouchPhase;
	import starling.textures.Texture;
	import starling.utils.Line;

	public class HandleSheet extends Sprite
	{

		/** 點擊 將物件移動到前方 **/
		protected var _touchBringToFront:Boolean
		public function get touchBringToFront():Boolean { return _touchBringToFront;}
		public function set touchBringToFront(value:Boolean):void{ _touchBringToFront = value; }

		/** 是否選擇 **/
		protected var _selected:Boolean
		public function get selected():Boolean { return _selected;}
		public function set selected(value:Boolean):void
		{
			if(value!=_selected){
				_selected = value;
				_selectedGroup.visible = _selected
			}
		}

		/** 是否有使用 控制按鈕 Read Only **/
		protected var _useCtrlButton:Boolean
		public function get useCtrlButton():Boolean
		{
			return _useCtrlButton;
		}

//		public function set useCtrlButton(value:Boolean):void
//		{
//			_useCtrlButton = value;
//		}

		//Const Vars
		protected static const CTRL_BUTTON_NAME:String = "HandlectrlBtn"
		public static const ALIGN_CENTER:String = "center"
		public static const ALIGN_LT:String = "LT"

		//UI
		protected var _contents:DisplayObject
		protected var _ctrlButton:DisplayObject
		protected var _selectedGroup:Sprite
		protected var _shape:Shape

		public function HandleSheet(contents:DisplayObject=null)
		{
			addEventListener(TouchEvent.TOUCH, onTouch);
			useHandCursor = true;

			_touchBringToFront = true

			if (contents)
			{
				var _w:Number = contents.width
				var _h:Number = contents.height
				var _halfW:Number = _w/2
				var _halfH:Number= _h/2
				//將物件置中
				contents.x = int(_halfW * -1);
				contents.y = int(_halfH * -1);
				addChild(contents);
				_contents = contents

			}
			//init SelectGroup
			initSelectedGroup()
		}

		/** 初始化選擇的群組**/
		protected function initSelectedGroup():void
		{

			_selectedGroup = new Sprite()
			this.addChild(this._selectedGroup);

			_shape = new Shape()
			updateLine()
			_selectedGroup.addChild(_shape)
			_selectedGroup.visible = _selected

		}

		/**重新計算**/
		protected function render(scale=1):void
		{
			updateLine(scale)
		}

		/**更新框線**/
		protected function updateLine(scale=1):void
		{
			if(_contents && _shape){
				_shape.graphics.lineStyle(10,0xFFffFF);
					_shape.graphics.clear();

					System.gc();

					var _w:Number = _contents.width
					var _h:Number = _contents.height
					var _halfW:Number = _w/2
					var _halfH:Number= _h/2

					_shape.graphics.lineStyle(2 * scale,0xFFFFFF);
					//_shape.graphics.drawRoundRect( -_halfW, -_halfH, _w, _h, 10 );
					_shape.graphics.drawRect(-_halfW, -_halfH, _w, _h);

					//trace("scale",scale)

			}
		}

		public function setCtrlButtonInitByTexture(upTexture:Texture , downTexture:Texture = null ):void
		{
			_useCtrlButton = true

			if ( upTexture !=null)
			{
				_ctrlButton = new Button(upTexture,"",downTexture);

				setCtrlButtonInitByObject(_ctrlButton )
			}else{
				throw new ArgumentError("Texture is Null!")
			}
		}

		/** 設定控制按鈕 by 物件 可以是 image sprite button **/
		public function setCtrlButtonInitByObject( obj:DisplayObject ):void
		{
			_useCtrlButton = true

			if (_contents)
			{
				_ctrlButton = obj

				var _w:Number = _contents.width
				var _h:Number = _contents.height
				var _halfW:Number = _w/2
				var _halfH:Number= _h/2
				//放置右上角
				_ctrlButton.x = int(_halfW)
				_ctrlButton.y = - int(_halfH)

				//置中對齊
				_ctrlButton.x -= int(_ctrlButton.width/2)
				_ctrlButton.y -= int(_ctrlButton.height/2)

				_selectedGroup.addChild(_ctrlButton)
			}
		}

		protected function onTouch(event:TouchEvent):void
		{
			var touches:Vector.<Touch> = event.getTouches(this, TouchPhase.MOVED);
			var touch:Touch

			touch = event.getTouch(this, TouchPhase.BEGAN);
			onTouchBegan(touch)

			//一隻手指
			if (touches.length == 1)
			{
				//檢查是否是Ctrl
				if(isTouchCtrlButton(event.target) )
				{
					resizeAndRotationByCtrlButton(touches)
				}
				else
				{
					//一隻手指平移
					var delta:Point = touches[0].getMovement(parent);

					x += delta.x;
					y += delta.y;
				}
			}
			else if (touches.length == 2)
			{
				//兩隻手指
				resizeAndRotationByTwoFingers(touches)
			}

			touch  = event.getTouch(this, TouchPhase.ENDED);
			onTouchEnd(touch)

		}

		/**當touch開始**/
		protected function onTouchBegan(touch:Touch):void
		{
			//touch 開始要做的事情
			if(touch){
				if(_touchBringToFront){
					parent.addChild(this);
				}
				selected = true
			}

		}

		/**當touch結束**/
		protected function onTouchEnd(touch:Touch):void
		{

		}

		/**檢查是否touch 控制按鈕**/
		protected function isTouchCtrlButton(target:*):Boolean
		{

			var _do		:DisplayObject
			var _doc	:DisplayObjectContainer

			if(_ctrlButton is  DisplayObjectContainer){
				_doc = _ctrlButton as DisplayObjectContainer
				return _doc.contains(target)
			}
			else if(_ctrlButton is DisplayObject)
			{
				_do = _ctrlButton as DisplayObject
				return _do == target
			}
			return false
		}

		/** 單隻手指使用控制按鈕 放大縮小旋轉 **/
		protected function resizeAndRotationByCtrlButton(touches:Vector.<Touch>):void
		{

			var touchA:Touch = touches[0];
			var touchB:Touch = touchA.clone();		//模擬B點
			var n:Point = touchA.getLocation(this);
			//鏡射A點坐標
			touchB.globalX = n.x * 1
			touchB.globalY = n.y * -1

			var currentPosA:Point  = touchA.getLocation(this);
			var previousPosA:Point = touchA.getPreviousLocation(this);

			//鏡射A點
			var currentPosB:Point  = currentPosA.clone();
			currentPosB.x *=-1;
			currentPosB.y *=-1;
			//鏡射A點
			var previousPosB:Point  = previousPosA.clone();
			previousPosB.x *=-1;
			previousPosB.y *=-1;

			var currentVector:Point  = currentPosA.subtract(currentPosB);
			var previousVector:Point = previousPosA.subtract(previousPosB);

			var currentAngle:Number  = Math.atan2(currentVector.y, currentVector.x);
			var previousAngle:Number = Math.atan2(previousVector.y, previousVector.x);

			var deltaAngle:Number = currentAngle - previousAngle;

			// rotate
			rotation += deltaAngle;

			// scale
			var sizeDiff:Number = currentVector.length / previousVector.length;
			scaleX *= sizeDiff;
			scaleY *= sizeDiff;

			//_ctrlButton 保持原比例
			if(_ctrlButton){
				_ctrlButton.scaleX /= sizeDiff
				_ctrlButton.scaleY /= sizeDiff
			}

			render(_ctrlButton.scaleX)

		}

		/** 使用兩隻手指 使用放大縮小旋轉 保留官方功能 **/
		protected function resizeAndRotationByTwoFingers(touches:Vector.<Touch>):void
		{
			//保留官方原本兩隻手指操作的功能
			// two fingers touching -> rotate and scale
			var touchA:Touch = touches[0];
			var touchB:Touch = touches[1];

			var currentPosA:Point  = touchA.getLocation(parent);
			var previousPosA:Point = touchA.getPreviousLocation(parent);
			var currentPosB:Point  = touchB.getLocation(parent);
			var previousPosB:Point = touchB.getPreviousLocation(parent);

			var currentVector:Point  = currentPosA.subtract(currentPosB);
			var previousVector:Point = previousPosA.subtract(previousPosB);

			var currentAngle:Number  = Math.atan2(currentVector.y, currentVector.x);
			var previousAngle:Number = Math.atan2(previousVector.y, previousVector.x);
			var deltaAngle:Number = currentAngle - previousAngle;

			// update pivot point based on previous center
			var previousLocalA:Point  = touchA.getPreviousLocation(this);
			var previousLocalB:Point  = touchB.getPreviousLocation(this);
			trace(previousLocalA , previousLocalB)

			pivotX = (previousLocalA.x + previousLocalB.x) * 0.5;
			pivotY = (previousLocalA.y + previousLocalB.y) * 0.5;

			// update location based on the current center
			x = (currentPosA.x + currentPosB.x) * 0.5;
			y = (currentPosA.y + currentPosB.y) * 0.5;

			// rotate
			rotation += deltaAngle;

			// scale
			var sizeDiff:Number = currentVector.length / previousVector.length;
			scaleX *= sizeDiff;
			scaleY *= sizeDiff;

			render(_ctrlButton.scaleX)
		}

		public override function dispose():void
		{
			removeEventListener(TouchEvent.TOUCH, onTouch);
			if(_contents){
				_contents.dispose()
			}
			super.dispose();
		}
	}
}

add 外框線

add 外框線


package model
{
	import dragonBones.Armature;
	import dragonBones.Bone;
	import dragonBones.animation.WorldClock;
	import dragonBones.events.AnimationEvent;
	import dragonBones.factorys.BaseFactory;
	import dragonBones.objects.BoneData;
	import dragonBones.objects.SkeletonData;
	import dragonBones.objects.XMLDataParser;
	import dragonBones.textures.NativeTextureAtlas;
	import dragonBones.utils.ConstValues;
	import dragonBones.utils.dragonBones_internal;

	import flash.errors.IllegalOperationError;
	import flash.utils.ByteArray;

	import message.MessageDispatcher;

	import mx.collections.XMLListCollection;

	import utils.GlobalConstValues;
	import utils.TextureUtil;
	import utils.movePivotToSkeleton;

	use namespace dragonBones_internal;

	[Bindable]
	/**
	 * Manage imported data
	 */
	public class ImportDataProxy
	{
		private static var _instance:ImportDataProxy
		public static function getInstance():ImportDataProxy
		{
			if(!_instance)
			{
				_instance = new ImportDataProxy();
			}
			return _instance;
		}

		public static function getElementByName(xmlList:XMLList, name:String = null, returnFirst:Boolean = false):XML
		{
			if(xmlList)
			{
				if(name)
				{
					return XMLDataParser.getElementsByAttribute(xmlList, ConstValues.A_NAME, name)[0];
				}
				if(returnFirst)
				{
					return xmlList[0];
				}
			}
			return null;
		}

		public var armaturesMC:XMLListCollection;

		public var isTextureChanged:Boolean;
		public var isExportedSource:Boolean;

		private var _armaturesXMLList:XMLList;
		private var _animationsXMLList:XMLList;
		private var _baseFactory:BaseFactory;

		public function get skeletonName():String
		{
			return _skeletonXML?_skeletonXML.attribute(ConstValues.A_NAME):"";
		}

		public function get frameRate():int
		{
			return int(_skeletonXML.attribute(ConstValues.A_FRAME_RATE));
		}

		private var _skeletonXML:XML;
		public function get skeletonXML():XML
		{
			return _skeletonXML;
		}

		private var _textureAtlasXML:XML;
		public function get textureAtlasXML():XML
		{
			return _textureAtlasXML;
		}

		private var _armatureDataProxy:ArmatureDataProxy;
		public function get armatureDataProxy():ArmatureDataProxy
		{
			return _armatureDataProxy;
		}

		private var _animationDataProxy:AnimationDataProxy;
		public function get animationDataProxy():AnimationDataProxy
		{
			return _animationDataProxy;
		}

		private var _skeletonData:SkeletonData;
		public function get skeletonData():SkeletonData
		{
			return _skeletonData;
		}
		private function set skeletonData(value:SkeletonData)
		{
			_skeletonData = value;
		}

		private var _textureAtlas:NativeTextureAtlas;
		public function get textureAtlas():NativeTextureAtlas
		{
			return _textureAtlas;
		}

		public var textureBytes:ByteArray;

		private var _armature:Armature;
		public function get armature():Armature
		{
			return _armature;
		}

		public function ImportDataProxy()
		{
			if (_instance)
			{
				throw new IllegalOperationError("Singleton already constructed!");
			}
			armaturesMC = new XMLListCollection();

			_armatureDataProxy = new ArmatureDataProxy();
			_animationDataProxy = new AnimationDataProxy();
			_baseFactory = new BaseFactory();
		}

		public function setData(skeletonXML:XML, textureAtlasXML:XML, textureData:Object, textureBytes:ByteArray, isExportedSource:Boolean):void
		{
			disposeArmature();

			if(_skeletonData)
			{
				_baseFactory.removeSkeletonData(_skeletonData.name);
				_skeletonData.dispose();
			}

			if(_textureAtlas)
			{
				_baseFactory.removeTextureAtlas(_textureAtlas.name);
				_textureAtlas.dispose();
			}

			isTextureChanged = false;

			_skeletonXML = skeletonXML;
			_textureAtlasXML = textureAtlasXML;
			this.textureBytes = textureBytes;
			this.isExportedSource = isExportedSource;

			movePivotToSkeleton(_skeletonXML, _textureAtlasXML);

			_armaturesXMLList = _skeletonXML.elements(ConstValues.ARMATURES).elements(ConstValues.ARMATURE);
			_animationsXMLList = _skeletonXML.elements(ConstValues.ANIMATIONS).elements(ConstValues.ANIMATION);

			armaturesMC.source = _armaturesXMLList;

			skeletonData = XMLDataParser.parseSkeletonData(skeletonXML);
			_textureAtlas = new NativeTextureAtlas(textureData, textureAtlasXML)
			_textureAtlas.movieClipToBitmapData();
			_baseFactory.addSkeletonData(_skeletonData);
			_baseFactory.addTextureAtlas(_textureAtlas);

			MessageDispatcher.dispatchEvent(MessageDispatcher.CHANGE_IMPORT_DATA, skeletonName);

			armatureDataProxy.setData(getArmatureXMLByName());
		}

		public function changeRenderArmature(armatureName:String):void
		{
			disposeArmature();

			_armature = _baseFactory.buildArmature(armatureName);
			_armature.addEventListener(dragonBones.events.AnimationEvent.MOVEMENT_CHANGE, armatureEventHandler);
			_armature.addEventListener(dragonBones.events.AnimationEvent.START, armatureEventHandler);
			_armature.addEventListener(dragonBones.events.AnimationEvent.COMPLETE, armatureEventHandler);
			WorldClock.clock.add(_armature);
		}

		public function render():void
		{
			WorldClock.update();
		}

		public function updateTextures():void
		{
			if(isExportedSource || !skeletonName)
			{
				return;
			}
			TextureUtil.packTextures(SettingDataProxy.getInstance().textureMaxWidth, SettingDataProxy.getInstance().texturePadding, textureAtlasXML);
			JSFLProxy.getInstance().packTextures(textureAtlasXML);
			isTextureChanged = true;
		}

		public function getArmatureXMLByName(name:String = null):XML
		{
			return getElementByName(_armaturesXMLList, name, true);
		}

		public function getAnimationXMLByName(name:String = null):XML
		{
			return getElementByName(_animationsXMLList, name, true);
		}

		public function updateArmatureBoneOrigin(boneName:String):void
		{
			var armatureName:String = armatureDataProxy.armatureName;
			updateOrigin(_armature, armatureName, boneName);
		}

		private function armatureEventHandler(e:AnimationEvent):void
		{
			switch(e.type)
			{
				case dragonBones.events.AnimationEvent.MOVEMENT_CHANGE:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_CHANGE, e.movementID);
					break;
				case dragonBones.events.AnimationEvent.START:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_START, e.movementID);
					break;
				case dragonBones.events.AnimationEvent.COMPLETE:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_COMPLETE, e.movementID);
					break;
			}
		}

		private function updateOrigin(armature:Armature, armatureName:String, boneName:String):void
		{
			if(armature)
			{
				if(armature.name == armatureName)
				{
					var boneData:BoneData = _skeletonData.getArmatureData(armatureName).getBoneData(boneName);
					var bone:Bone = armature.getBone(boneName);
					bone._origin.copy(boneData);
					armature.addBone(bone, boneData.parent);
				}
				for each(bone in armature._boneDepthList)
				{
					updateOrigin(bone.childArmature, armatureName, boneName);
				}
			}
		}

		private function disposeArmature():void
		{
			if(_armature)
			{
				WorldClock.clock.remove(_armature);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.MOVEMENT_CHANGE, armatureEventHandler);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.START, armatureEventHandler);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.COMPLETE, armatureEventHandler);
				_armature.dispose();
			}
			_armature = null;
		}
	}
}

package model
{
	import dragonBones.Armature;
	import dragonBones.Bone;
	import dragonBones.animation.WorldClock;
	import dragonBones.events.AnimationEvent;
	import dragonBones.factorys.BaseFactory;
	import dragonBones.objects.BoneData;
	import dragonBones.objects.SkeletonData;
	import dragonBones.objects.XMLDataParser;
	import dragonBones.textures.NativeTextureAtlas;
	import dragonBones.utils.ConstValues;
	import dragonBones.utils.dragonBones_internal;

	import flash.errors.IllegalOperationError;
	import flash.utils.ByteArray;

	import message.MessageDispatcher;

	import mx.collections.XMLListCollection;

	import utils.GlobalConstValues;
	import utils.TextureUtil;
	import utils.movePivotToSkeleton;

	use namespace dragonBones_internal;

	[Bindable]
	/**
	 * Manage imported data
	 */
	public class ImportDataProxy
	{
		private static var _instance:ImportDataProxy
		public static function getInstance():ImportDataProxy
		{
			if(!_instance)
			{
				_instance = new ImportDataProxy();
			}
			return _instance;
		}

		public static function getElementByName(xmlList:XMLList, name:String = null, returnFirst:Boolean = false):XML
		{
			if(xmlList)
			{
				if(name)
				{
					return XMLDataParser.getElementsByAttribute(xmlList, ConstValues.A_NAME, name)[0];
				}
				if(returnFirst)
				{
					return xmlList[0];
				}
			}
			return null;
		}

		public var armaturesMC:XMLListCollection;

		public var isTextureChanged:Boolean;
		public var isExportedSource:Boolean;

		private var _armaturesXMLList:XMLList;
		private var _animationsXMLList:XMLList;
		private var _baseFactory:BaseFactory;

		public function get skeletonName():String
		{
			return _skeletonXML?_skeletonXML.attribute(ConstValues.A_NAME):"";
		}

		public function get frameRate():int
		{
			return int(_skeletonXML.attribute(ConstValues.A_FRAME_RATE));
		}

		private var _skeletonXML:XML;
		public function get skeletonXML():XML
		{
			return _skeletonXML;
		}

		private var _textureAtlasXML:XML;
		public function get textureAtlasXML():XML
		{
			return _textureAtlasXML;
		}

		private var _armatureDataProxy:ArmatureDataProxy;
		public function get armatureDataProxy():ArmatureDataProxy
		{
			return _armatureDataProxy;
		}

		private var _animationDataProxy:AnimationDataProxy;
		public function get animationDataProxy():AnimationDataProxy
		{
			return _animationDataProxy;
		}

		private var _skeletonData:SkeletonData;
		public function get skeletonData():SkeletonData
		{
			return _skeletonData;
		}
		private function set skeletonData(value:SkeletonData):void
		{
			_skeletonData = value;
		}

		private var _textureAtlas:NativeTextureAtlas;
		public function get textureAtlas():NativeTextureAtlas
		{
			return _textureAtlas;
		}

		public var textureBytes:ByteArray;

		private var _armature:Armature;
		public function get armature():Armature
		{
			return _armature;
		}

		public function ImportDataProxy()
		{
			if (_instance)
			{
				throw new IllegalOperationError("Singleton already constructed!");
			}
			armaturesMC = new XMLListCollection();

			_armatureDataProxy = new ArmatureDataProxy();
			_animationDataProxy = new AnimationDataProxy();
			_baseFactory = new BaseFactory();
		}

		public function setData(skeletonXML:XML, textureAtlasXML:XML, textureData:Object, textureBytes:ByteArray, isExportedSource:Boolean):void
		{
			disposeArmature();

			if(_skeletonData)
			{
				_baseFactory.removeSkeletonData(_skeletonData.name);
				_skeletonData.dispose();
			}

			if(_textureAtlas)
			{
				_baseFactory.removeTextureAtlas(_textureAtlas.name);
				_textureAtlas.dispose();
			}

			isTextureChanged = false;

			_skeletonXML = skeletonXML;
			_textureAtlasXML = textureAtlasXML;
			this.textureBytes = textureBytes;
			this.isExportedSource = isExportedSource;

			movePivotToSkeleton(_skeletonXML, _textureAtlasXML);

			_armaturesXMLList = _skeletonXML.elements(ConstValues.ARMATURES).elements(ConstValues.ARMATURE);
			_animationsXMLList = _skeletonXML.elements(ConstValues.ANIMATIONS).elements(ConstValues.ANIMATION);

			armaturesMC.source = _armaturesXMLList;

			skeletonData = XMLDataParser.parseSkeletonData(skeletonXML);
			_textureAtlas = new NativeTextureAtlas(textureData, textureAtlasXML)
			_textureAtlas.movieClipToBitmapData();
			_baseFactory.addSkeletonData(_skeletonData);
			_baseFactory.addTextureAtlas(_textureAtlas);

			MessageDispatcher.dispatchEvent(MessageDispatcher.CHANGE_IMPORT_DATA, skeletonName);

			armatureDataProxy.setData(getArmatureXMLByName());
		}

		public function changeRenderArmature(armatureName:String):void
		{
			disposeArmature();

			_armature = _baseFactory.buildArmature(armatureName);
			_armature.addEventListener(dragonBones.events.AnimationEvent.MOVEMENT_CHANGE, armatureEventHandler);
			_armature.addEventListener(dragonBones.events.AnimationEvent.START, armatureEventHandler);
			_armature.addEventListener(dragonBones.events.AnimationEvent.COMPLETE, armatureEventHandler);
			WorldClock.clock.add(_armature);
		}

		public function render():void
		{
			WorldClock.update();
		}

		public function updateTextures():void
		{
			if(isExportedSource || !skeletonName)
			{
				return;
			}
			TextureUtil.packTextures(SettingDataProxy.getInstance().textureMaxWidth, SettingDataProxy.getInstance().texturePadding, textureAtlasXML);
			JSFLProxy.getInstance().packTextures(textureAtlasXML);
			isTextureChanged = true;
		}

		public function getArmatureXMLByName(name:String = null):XML
		{
			return getElementByName(_armaturesXMLList, name, true);
		}

		public function getAnimationXMLByName(name:String = null):XML
		{
			return getElementByName(_animationsXMLList, name, true);
		}

		public function updateArmatureBoneOrigin(boneName:String):void
		{
			var armatureName:String = armatureDataProxy.armatureName;
			updateOrigin(_armature, armatureName, boneName);
		}

		private function armatureEventHandler(e:AnimationEvent):void
		{
			switch(e.type)
			{
				case dragonBones.events.AnimationEvent.MOVEMENT_CHANGE:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_CHANGE, e.movementID);
					break;
				case dragonBones.events.AnimationEvent.START:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_START, e.movementID);
					break;
				case dragonBones.events.AnimationEvent.COMPLETE:
					MessageDispatcher.dispatchEvent(MessageDispatcher.MOVEMENT_COMPLETE, e.movementID);
					break;
			}
		}

		private function updateOrigin(armature:Armature, armatureName:String, boneName:String):void
		{
			if(armature)
			{
				if(armature.name == armatureName)
				{
					var boneData:BoneData = _skeletonData.getArmatureData(armatureName).getBoneData(boneName);
					var bone:Bone = armature.getBone(boneName);
					bone._origin.copy(boneData);
					armature.addBone(bone, boneData.parent);
				}
				for each(bone in armature._boneDepthList)
				{
					updateOrigin(bone.childArmature, armatureName, boneName);
				}
			}
		}

		private function disposeArmature():void
		{
			if(_armature)
			{
				WorldClock.clock.remove(_armature);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.MOVEMENT_CHANGE, armatureEventHandler);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.START, armatureEventHandler);
				_armature.removeEventListener(dragonBones.events.AnimationEvent.COMPLETE, armatureEventHandler);
				_armature.dispose();
			}
			_armature = null;
		}
	}
}

declare function return value type

declare function return value type