<commit_before>var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');

var test_helper = require('../support/test_helper');
var Step = require('step');
var _ = require('underscore');

suite('mapconfig_named_layers_adapter', function() {

    // configure redis pool instance to use in tests
    var redisPool = RedisPool(global.environment.redis);

    var templateMaps = new TemplateMaps(redisPool, {
        max_user_templates: global.environment.maxUserTemplates
    });

    var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);

    var wadusLayer = {
        type: 'cartodb',
        options: {
            sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
            cartocss: '#layer { marker-fill: <%= color %>; }',
            cartocss_version: '2.3.0'
        }
    };

    var wadusMapnikLayer = {
        type: 'mapnik',
        options: {
            sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
            cartocss: '#layer { polygon-fill: <%= polygon_color %>; }',
            cartocss_version: '2.3.0'
        }
    };

    var username = 'me';

    var templateName = 'valid_template';
    var template = {
        version: '0.0.1',
        name: templateName,
        auth: {
            method: 'open'
        },
        "placeholders": {
            "color": {
                "type": "css_color",
                "default": "#cc3300"
            }
        },
        layergroup: {
            layers: [
                wadusLayer
            ]
        }
    };

    var tokenAuthTemplateName = 'auth_valid_template';
    var tokenAuthTemplate = {
        version: '0.0.1',
        name: tokenAuthTemplateName,
        auth: {
            method: 'token',
            valid_tokens: ['valid1', 'valid2']
        },
        layergroup: {
            layers: [
                wadusLayer
            ]
        }
    };

    var multipleLayersTemplateName = 'multiple_valid_template';
    var multipleLayersTemplate = {
        version: '0.0.1',
        name: multipleLayersTemplateName,
        auth: {
            method: 'token',
            valid_tokens: ['valid1', 'valid2']
        },
        "placeholders": {
            "polygon_color": {
                "type": "css_color",
                "default": "green"
            },
            "color": {
                "type": "css_color",
                "default": "red"
            }
        },
        layergroup: {
            layers: [
                wadusMapnikLayer,
                wadusLayer
            ]
        }
    };

    var namedMapLayer = {
        type: 'named',
        options: {
            name: templateName,
            config: {},
            auth_tokens: []
        }
    };

    var nestedNamedMapTemplateName = 'auth_valid_template';
    var nestedNamedMapTemplate = {
        version: '0.0.1',
        name: nestedNamedMapTemplateName,
        auth: {
            method: 'open'
        },
        layergroup: {
            layers: [
                namedMapLayer
            ]
        }
    };

    function makeNamedMapLayerConfig(options) {
        return {
            version: '1.3.0',
            layers: [
                {
                    type: 'named',
                    options: options
                }
            ]
        };
    }

    suiteSetup(function(done) {
        templateMaps.addTemplate(username, template, done);
    });

    test('should fail for named map layer with missing name', function(done) {

        assert.response(server,
            healthCheckRequest,
            {
                status: 200
            },
            function (res, err) {
                assert.ok(!err);

                var parsed = JSON.parse(res.body);

                assert.ok(parsed.enabled);
                assert.ok(parsed.ok);

                done();
            }
        );

        var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
            config: {}
        });
        mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, function(err, layers) {
            assert.ok(err);
            assert.ok(!layers);
            assert.equal(err.message, 'Missing Named Map `name` in layer options');
            done();
        });
    });

    test('should fail for non-existing template name', function(done) {
        var missingTemplateName = 'wadus';
        var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
            name: missingTemplateName
        });
        mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, function(err, layers) {
            assert.ok(err);
            assert.ok(!layers);
            assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found");
            done();
        });
    });

    test('should fail if not properly authorized', function(done) {
        templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: tokenAuthTemplateName
            });
            mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(err);
                assert.ok(!layers);
                assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation");

                templateMaps.delTemplate(username, tokenAuthTemplateName, done);
            });
        });
    });

    test('should fail for nested named map layers', function(done) {
        templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: nestedNamedMapTemplateName
            });
            mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(err);
                assert.ok(!layers);
                assert.equal(err.message, 'Nested named layers are not allowed');

                templateMaps.delTemplate(username, nestedNamedMapTemplateName, done);
            });
        });
    });

    test('should return an expanded list of layers for a named map layer', function(done) {
        var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({
            name: templateName
        });
        mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, function(err, layers) {
            assert.ok(!err);
            assert.ok(layers.length, 1);
            assert.ok(layers[0].type, 'cartodb');
            done();
        });
    });

    test('should return on auth=token with valid tokens provided', function(done) {
        templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: tokenAuthTemplateName,
                auth_tokens: ['valid1']
            });
            mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 1);

                templateMaps.delTemplate(username, tokenAuthTemplateName, done);
            });
        });
    });

    test('should return an expanded list of layers for a named map layer, multiple layers version', function(done) {
        templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: multipleLayersTemplateName,
                auth_tokens: ['valid2']
            });
            mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 2);

                assert.equal(layers[0].type, 'mapnik');
                assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }');

                assert.equal(layers[1].type, 'cartodb');
                assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }');

                templateMaps.delTemplate(username, multipleLayersTemplateName, done);
            });
        });
    });

    test('should replace template params with the given config', function(done) {
        templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var color = '#cc3300',
                polygonColor = '#ff9900';

            var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: multipleLayersTemplateName,
                config: {
                    polygon_color: polygonColor,
                    color: color
                },
                auth_tokens: ['valid2']
            });
            mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 2);

                assert.equal(layers[0].type, 'mapnik');
                assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }');

                assert.equal(layers[1].type, 'cartodb');
                assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }');

                templateMaps.delTemplate(username, multipleLayersTemplateName, done);
            });
        });
    });

    suiteTeardown(function(done) {
        templateMaps.delTemplate(username, templateName, done);
    });
});
<commit_msg>Fix integration tests as I messed with the filename<commit_after>var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');

var test_helper = require('../support/test_helper');
var Step = require('step');
var _ = require('underscore');

suite('mapconfig_named_layers_adapter', function() {

    // configure redis pool instance to use in tests
    var redisPool = RedisPool(global.environment.redis);

    var templateMaps = new TemplateMaps(redisPool, {
        max_user_templates: global.environment.maxUserTemplates
    });

    var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);

    var wadusLayer = {
        type: 'cartodb',
        options: {
            sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
            cartocss: '#layer { marker-fill: <%= color %>; }',
            cartocss_version: '2.3.0'
        }
    };

    var wadusMapnikLayer = {
        type: 'mapnik',
        options: {
            sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
            cartocss: '#layer { polygon-fill: <%= polygon_color %>; }',
            cartocss_version: '2.3.0'
        }
    };

    var username = 'me';

    var templateName = 'valid_template';
    var template = {
        version: '0.0.1',
        name: templateName,
        auth: {
            method: 'open'
        },
        "placeholders": {
            "color": {
                "type": "css_color",
                "default": "#cc3300"
            }
        },
        layergroup: {
            layers: [
                wadusLayer
            ]
        }
    };

    var tokenAuthTemplateName = 'auth_valid_template';
    var tokenAuthTemplate = {
        version: '0.0.1',
        name: tokenAuthTemplateName,
        auth: {
            method: 'token',
            valid_tokens: ['valid1', 'valid2']
        },
        layergroup: {
            layers: [
                wadusLayer
            ]
        }
    };

    var multipleLayersTemplateName = 'multiple_valid_template';
    var multipleLayersTemplate = {
        version: '0.0.1',
        name: multipleLayersTemplateName,
        auth: {
            method: 'token',
            valid_tokens: ['valid1', 'valid2']
        },
        "placeholders": {
            "polygon_color": {
                "type": "css_color",
                "default": "green"
            },
            "color": {
                "type": "css_color",
                "default": "red"
            }
        },
        layergroup: {
            layers: [
                wadusMapnikLayer,
                wadusLayer
            ]
        }
    };

    var namedMapLayer = {
        type: 'named',
        options: {
            name: templateName,
            config: {},
            auth_tokens: []
        }
    };

    var nestedNamedMapTemplateName = 'nested_template';
    var nestedNamedMapTemplate = {
        version: '0.0.1',
        name: nestedNamedMapTemplateName,
        auth: {
            method: 'open'
        },
        layergroup: {
            layers: [
                namedMapLayer
            ]
        }
    };

    function makeNamedMapLayerConfig(options) {
        return {
            version: '1.3.0',
            layers: [
                {
                    type: 'named',
                    options: options
                }
            ]
        };
    }

    suiteSetup(function(done) {
        templateMaps.addTemplate(username, template, done);
    });

    test('should fail for named map layer with missing name', function(done) {
        var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
            config: {}
        });
        mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, function(err, layers) {
            assert.ok(err);
            assert.ok(!layers);
            assert.equal(err.message, 'Missing Named Map `name` in layer options');
            done();
        });
    });

    test('should fail for non-existing template name', function(done) {
        var missingTemplateName = 'wadus';
        var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
            name: missingTemplateName
        });
        mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, function(err, layers) {
            assert.ok(err);
            assert.ok(!layers);
            assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found");
            done();
        });
    });

    test('should fail if not properly authorized', function(done) {
        templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: tokenAuthTemplateName
            });
            mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(err);
                assert.ok(!layers);
                assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation");

                templateMaps.delTemplate(username, tokenAuthTemplateName, done);
            });
        });
    });

    test('should fail for nested named map layers', function(done) {
        templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: nestedNamedMapTemplateName
            });
            mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(err);
                assert.ok(!layers);
                assert.equal(err.message, 'Nested named layers are not allowed');

                templateMaps.delTemplate(username, nestedNamedMapTemplateName, done);
            });
        });
    });

    test('should return an expanded list of layers for a named map layer', function(done) {
        var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({
            name: templateName
        });
        mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, function(err, layers) {
            assert.ok(!err);
            assert.ok(layers.length, 1);
            assert.ok(layers[0].type, 'cartodb');
            done();
        });
    });

    test('should return on auth=token with valid tokens provided', function(done) {
        templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: tokenAuthTemplateName,
                auth_tokens: ['valid1']
            });
            mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 1);

                templateMaps.delTemplate(username, tokenAuthTemplateName, done);
            });
        });
    });

    test('should return an expanded list of layers for a named map layer, multiple layers version', function(done) {
        templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: multipleLayersTemplateName,
                auth_tokens: ['valid2']
            });
            mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 2);

                assert.equal(layers[0].type, 'mapnik');
                assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }');

                assert.equal(layers[1].type, 'cartodb');
                assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }');

                templateMaps.delTemplate(username, multipleLayersTemplateName, done);
            });
        });
    });

    test('should replace template params with the given config', function(done) {
        templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
            if (err) {
                return done(err);
            }

            var color = '#cc3300',
                polygonColor = '#ff9900';

            var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
                name: multipleLayersTemplateName,
                config: {
                    polygon_color: polygonColor,
                    color: color
                },
                auth_tokens: ['valid2']
            });
            mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
                assert.ok(!err);
                assert.equal(layers.length, 2);

                assert.equal(layers[0].type, 'mapnik');
                assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }');

                assert.equal(layers[1].type, 'cartodb');
                assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }');

                templateMaps.delTemplate(username, multipleLayersTemplateName, done);
            });
        });
    });

    suiteTeardown(function(done) {
        templateMaps.delTemplate(username, templateName, done);
    });
});
<|endoftext|>


<commit_before>var expect = require('chai').expect,
    server = require('../../fixtures/server'),
    encoder = require('postman-url-encoder/encoder');

describe('URL encoding', function () {
    var echoServer = server.createRawEchoServer();

    before(function (done) {
        echoServer.listen(done);
    });

    after(function (done) {
        echoServer.destroy(done);
    });

    describe('path', function () {
        it('should percent-encode C0 control codes', function (done) {
            var i,
                pathToSend = [],
                expectedPath = [];

            for (i = 0; i < 32; i++) {
                pathToSend.push(String.fromCharCode(i));
                expectedPath.push(encoder.percentEncodeCharCode(i));
            }

            pathToSend.push(String.fromCharCode(127));
            expectedPath.push(encoder.percentEncodeCharCode(127));

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start/${pathToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentPath = response.slice(response.indexOf('/start/') + 7, response.indexOf('/end')).split('/');

                expect(sentPath).to.eql(expectedPath);
                done();
            });
        });

        it('should percent-encode SPACE, ("), (<), (>), (`), (#), (?), ({), and (})', function (done) {
            var i,
                char,
                pathToSend = [],
                expectedPath = [],
                encodeSet = new Set([' ', '"', '<', '>', '`', '#', '?', '{', '}']),
                reservedPathChars = new Set(['#', '?', '/', '\\']);

            for (i = 32; i < 127; i++) {
                char = String.fromCharCode(i);

                // don't add characters that can mess up parsing of path
                if (reservedPathChars.has(char)) { continue; }

                pathToSend.push(char);
                expectedPath.push(encodeSet.has(char) ? encoder.percentEncodeCharCode(i) : char);
            }

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start/${pathToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentPath = response.slice(response.indexOf('/start/') + 7, response.indexOf('/end')).split('/');

                expect(sentPath).to.eql(expectedPath);
                done();
            });
        });
    });

    describe('query', function () {
        it('should percent-encode C0 control codes', function (done) {
            var i,
                valueToSend = [],
                expectedValue = [];

            for (i = 0; i < 32; i++) {
                valueToSend.push(String.fromCharCode(i));
                expectedValue.push(encoder.percentEncodeCharCode(i));
            }

            valueToSend.push(String.fromCharCode(127));
            expectedValue.push(encoder.percentEncodeCharCode(127));

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start?q1=${valueToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sentValue from response
                    // response = <something>/start?q1=<sentValue>/end<something>
                    sentValue = response.slice(response.indexOf('/start?q1=') + 10, response.indexOf('/end'));

                sentValue = sentValue.split('/');

                expect(sentValue).to.eql(expectedValue);
                done();
            });
        });

        it('should percent-encode SPACE, ("), (#), (&), (\'), (<), (=), and (>)', function (done) {
            var i,
                char,
                valueToSend = [],
                expectedValue = [],
                encodeSet = new Set([' ', '"', '#', '&', '\'', '<', '=', '>']),
                reservedPathChars = new Set(['#', '&']);

            for (i = 32; i < 127; i++) {
                char = String.fromCharCode(i);

                // don't add characters that can mess up parsing of query
                if (reservedPathChars.has(char)) { continue; }

                valueToSend.push(char);
                expectedValue.push(encodeSet.has(char) ? encoder.percentEncodeCharCode(i) : char);
            }

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: {
                            url: `${echoServer.url}/start?q1=${valueToSend.join('\n')}/end`
                        }
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentValue = response.slice(response.indexOf('/start?q1=') + 10, response.indexOf('/end'));

                // split by '%0A' because original '\n' will be encoded to '%0A'
                sentValue = sentValue.split('%0A');

                expect(sentValue).to.eql(expectedValue);
                done();
            });
        });
    });
});
<commit_msg>Update tests for URL encoding<commit_after>var expect = require('chai').expect,
    server = require('../../fixtures/server'),
    encoder = require('postman-url-encoder/encoder');

describe('URL encoding', function () {
    var echoServer = server.createRawEchoServer();

    before(function (done) {
        echoServer.listen(done);
    });

    after(function (done) {
        echoServer.destroy(done);
    });

    describe('path', function () {
        it('should percent-encode C0 control codes', function (done) {
            var i,
                pathToSend = [],
                expectedPath = [];

            for (i = 0; i < 32; i++) {
                pathToSend.push(String.fromCharCode(i));
                expectedPath.push(encoder.percentEncodeCharCode(i));
            }

            pathToSend.push(String.fromCharCode(127));
            expectedPath.push(encoder.percentEncodeCharCode(127));

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start/${pathToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentPath = response.slice(response.indexOf('/start/') + 7, response.indexOf('/end')).split('/');

                expect(sentPath).to.eql(expectedPath);
                done();
            });
        });

        it('should percent-encode SPACE, ("), (<), (>), (`), (#), (?), ({), and (})', function (done) {
            var i,
                char,
                pathToSend = [],
                expectedPath = [],
                encodeSet = new Set([' ', '"', '<', '>', '`', '#', '?', '{', '}']),
                reservedPathChars = new Set(['#', '?', '/', '\\']);

            for (i = 32; i < 127; i++) {
                char = String.fromCharCode(i);

                // don't add characters that can mess up parsing of path
                if (reservedPathChars.has(char)) { continue; }

                pathToSend.push(char);
                expectedPath.push(encodeSet.has(char) ? encoder.percentEncodeCharCode(i) : char);
            }

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start/${pathToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentPath = response.slice(response.indexOf('/start/') + 7, response.indexOf('/end')).split('/');

                expect(sentPath).to.eql(expectedPath);
                done();
            });
        });
    });

    describe('query', function () {
        it('should percent-encode C0 control codes', function (done) {
            var i,
                valueToSend = [],
                expectedValue = [];

            for (i = 0; i < 32; i++) {
                valueToSend.push(String.fromCharCode(i));
                expectedValue.push(encoder.percentEncodeCharCode(i));
            }

            valueToSend.push(String.fromCharCode(127));
            expectedValue.push(encoder.percentEncodeCharCode(127));

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: `${echoServer.url}/start?q1=${valueToSend.join('/')}/end`
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sentValue from response
                    // response = <something>/start?q1=<sentValue>/end<something>
                    sentValue = response.slice(response.indexOf('/start?q1=') + 10, response.indexOf('/end'));

                sentValue = sentValue.split('/');

                expect(sentValue).to.eql(expectedValue);
                done();
            });
        });

        it('should percent-encode SPACE, ("), (#), (\'), (<), and (>)', function (done) {
            var i,
                char,
                valueToSend = [],
                expectedValue = [],
                encodeSet = new Set([' ', '"', '#', '\'', '<', '>']),
                reservedPathChars = new Set(['#']);

            for (i = 32; i < 127; i++) {
                char = String.fromCharCode(i);

                // don't add characters that can mess up parsing of query
                if (reservedPathChars.has(char)) { continue; }

                valueToSend.push(char);
                expectedValue.push(encodeSet.has(char) ? encoder.percentEncodeCharCode(i) : char);
            }

            this.run({
                requester: {
                    useWhatWGUrlParser: true
                },
                collection: {
                    item: {
                        request: {
                            url: `${echoServer.url}/start?q1=${valueToSend.join('\n')}/end`
                        }
                    }
                }
            }, function (err, result) {
                expect(err).to.not.be.ok;
                expect(result.response.calledOnce).to.be.ok;

                var response = result.response.getCall(0).args[2].stream.toString(),

                    // extract sent path from response
                    // response = <something>/start/<sentPath>/end<something>
                    sentValue = response.slice(response.indexOf('/start?q1=') + 10, response.indexOf('/end'));

                // split by '%0A' because original '\n' will be encoded to '%0A'
                sentValue = sentValue.split('%0A');

                expect(sentValue).to.eql(expectedValue);
                done();
            });
        });
    });
});
<|endoftext|>


<commit_before>/*
# vim: ts=2 sw=2 et
###############################################################################
# Copyright (c) 2014, Andreas Vogel andreas@wellenvogel.net
# parts of software from movable-type
# http://www.movable-type.co.uk/
# for their license see the file latlon.js
#
#  Permission is hereby granted, free of charge, to any person obtaining a
#  copy of this software and associated documentation files (the "Software"),
#  to deal in the Software without restriction, including without limitation
#  the rights to use, copy, modify, merge, publish, distribute, sublicense,
#  and/or sell copies of the Software, and to permit persons to whom the
#  Software is furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included
#  in all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
#  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
#  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#  DEALINGS IN THE SOFTWARE.
###############################################################################

icons partly from http://www.tutorial9.net/downloads/108-mono-icons-huge-set-of-minimal-icons/
                  http://ionicons.com/ (MIT license)
*/

avnav.provide('avnav.main');

/**
 * currently we must require all pages somewhere
 */

var propertyDefinitions=function(){
    return {
        layers:{
            ais: new avnav.util.Property(true,"AIS",avnav.util.PropertyType.CHECKBOX),
            track: new avnav.util.Property(true,"Track",avnav.util.PropertyType.CHECKBOX),
            nav: new avnav.util.Property(true,"Navigation",avnav.util.PropertyType.CHECKBOX),
            boat: new avnav.util.Property(true,"Boat",avnav.util.PropertyType.CHECKBOX),
            grid: new avnav.util.Property(true,"Grid",avnav.util.PropertyType.CHECKBOX),
            compass: new avnav.util.Property(true,"Compass",avnav.util.PropertyType.CHECKBOX)
        },
        forceMobile: new avnav.util.Property(false,"forceMobile",avnav.util.PropertyType.CHECKBOX),
        connectedMode:new avnav.util.Property(true,"connected",avnav.util.PropertyType.CHECKBOX),
        NM: new avnav.util.Property(1852), //one mile
        buttonUpdateTime: new avnav.util.Property( 500), //timer for button updates
        slideTime: new avnav.util.Property( 300), //time in ms for upzoom
        slideLevels: new avnav.util.Property( 3), //start with that many lower zoom levels
        maxUpscale: new avnav.util.Property(2), //2 levels upscale (otherwise we need too much mem)
        hideLower: new avnav.util.Property( true), //if set, hide smaller zoom layers when completely covered
        maxZoom: new avnav.util.Property( 21),  //only allow upscaling up to this zom level
        courseAverageFactor: new avnav.util.Property(0.5), //moving average for course up
        courseAverageTolerance: new avnav.util.Property(15,"Rotation Tolerance",avnav.util.PropertyType.RANGE,[1,30]), //tolerance for slow rotation
        minGridLedvel: new avnav.util.Property( 10),
        loggingEnabled: new avnav.util.Property( true),
        positionQueryTimeout: new avnav.util.Property( 1000,"Position",avnav.util.PropertyType.RANGE,[500,5000,10]), //1000ms
        trackQueryTimeout: new avnav.util.Property( 5000,"Track",avnav.util.PropertyType.RANGE,[500,10000,10]), //5s in ms
        routeQueryTimeout: new avnav.util.Property( 1000,"Route",avnav.util.PropertyType.RANGE,[500,10000,10]), //5s in ms
        bearingColor: new avnav.util.Property( "#DDA01F","Color",avnav.util.PropertyType.COLOR),
        bearingWidth: new avnav.util.Property( 3,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        routeColor: new avnav.util.Property( "#27413B","Color",avnav.util.PropertyType.COLOR),
        routeWidth: new avnav.util.Property( 2,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        routeWpSize:new avnav.util.Property( 7,"WPSize",avnav.util.PropertyType.RANGE,[5,30]),
        routeApproach: new avnav.util.Property( 200,"Approach(m)",avnav.util.PropertyType.RANGE,[20,2000]),
        routeShowLL: new avnav.util.Property(false,"showLatLon",avnav.util.PropertyType.CHECKBOX), //show latlon or leg course/len
        navCircleColor: new avnav.util.Property( "#D71038","Circle Color",avnav.util.PropertyType.COLOR),
        navCircleWidth: new avnav.util.Property( 1,"Circle Width",avnav.util.PropertyType.RANGE,[1,10]),
        navCircle1Radius: new avnav.util.Property( 300,"Circle 1 Radius(m)",avnav.util.PropertyType.RANGE,[0,5000,10]),
        navCircle2Radius: new avnav.util.Property( 1000,"Circle 2 Radius(m)",avnav.util.PropertyType.RANGE,[0,5000,10]),
        navCircle3Radius: new avnav.util.Property( 0,"Circle 3 Radius(m)",avnav.util.PropertyType.RANGE,[0,10000,10]),
        gpsXteMax:new avnav.util.Property( 1,"XTE(nm)",avnav.util.PropertyType.RANGE,[0.1,5,0.1,1]),
        trackColor: new avnav.util.Property( "#942eba","Color",avnav.util.PropertyType.COLOR),
        trackWidth: new avnav.util.Property( 3,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        trackInterval: new avnav.util.Property( 30,"Point Dist.(s)",avnav.util.PropertyType.RANGE,[5,300]), //seconds
        initialTrackLength: new avnav.util.Property( 24,"Length(h)",avnav.util.PropertyType.RANGE,[1,48]), //in h
        aisQueryTimeout: new avnav.util.Property( 5000,"AIS",avnav.util.PropertyType.RANGE,[1000,10000,10]), //ms
        aisDistance: new avnav.util.Property( 20,"AIS-Range(nm)",avnav.util.PropertyType.RANGE,[1,100]), //distance for AIS query in nm
        aisClickTolerance: new avnav.util.Property( 80,"Click Tolerance",avnav.util.PropertyType.RANGE,[10,100]),
        maxAisErrors: new avnav.util.Property( 3), //after that many errors AIS display will be switched off
        minAISspeed: new avnav.util.Property( 0.2), //minimal speed in kn that we consider when computing cpa/tcpa
        maxAisTPA: new avnav.util.Property( 3),    //max. computed AIS TPA time in h (otherwise we do not consider this)
        aisWarningCpa: new avnav.util.Property( 500,"AIS Warning-CPA(m)",avnav.util.PropertyType.RANGE,[100,5000,10]), //m for AIS warning (500m)
        aisWarningTpa: new avnav.util.Property( 900,"AIS-Warning-TPA(s)",avnav.util.PropertyType.RANGE,[30,3600,10]), //in s - max time for tpa warning (15min)
        aisTextSize:new avnav.util.Property( 14,"Text Size(px)",avnav.util.PropertyType.RANGE,[8,24]), //in px
        //images are not used any more, just keeping for fallback
        aisNormalImage: new avnav.util.Property( 'images/ais-default.png'),
        aisNearestImage: new avnav.util.Property( 'images/ais-nearest.png'),
        aisWarningImage: new avnav.util.Property( 'images/ais-warning.png'),
        statusQueryTimeout: new avnav.util.Property( 3000), //ms
        centerDisplayTimeout: new avnav.util.Property( 45000), //ms - auto hide measure display (0 - no auto hide)
        navUrl: new avnav.util.Property( "/viewer/avnav_navi.php"),
        maxGpsErrors: new avnav.util.Property( 3), //after that much invalid responses/timeouts the GPS is dead
        settingsName: new avnav.util.Property( "avnav.settings"), //storage name
        routingDataName: new avnav.util.Property( "avnav.routing"),
        routeName: new avnav.util.Property( "avnav.defaultRoute"),
        routingServerError: new avnav.util.Property(true,"ServerError",avnav.util.PropertyType.CHECKBOX), //notify comm errors to server
        routingTextSize:new avnav.util.Property( 14,"Text Size(px)",avnav.util.PropertyType.RANGE,[8,36]), //in px
        centerName: new avnav.util.Property( "avnav.center"),
        statusErrorImage: new avnav.util.Property( "images/RedBubble40.png"),
        statusOkImage: new avnav.util.Property( "images/GreenBubble40.png"),
        statusIcons: {
            INACTIVE: new avnav.util.Property( "images/GreyBubble40.png"),
            STARTED: new avnav.util.Property(  "images/YellowBubble40.png"),
            RUNNING: new avnav.util.Property(  "images/YellowBubble40.png"),
            NMEA: new avnav.util.Property(	  "images/GreenBubble40.png"),
            ERROR: new avnav.util.Property(	  "images/RedBubble40.png")
        },
        nightFade:new avnav.util.Property( 15,"NightDim(%)",avnav.util.PropertyType.RANGE,[1,99]), //in px
        nightChartFade:new avnav.util.Property( 30,"NightChartDim(%)",avnav.util.PropertyType.RANGE,[1,99]), //in %
        //all style members map to less variables
        style:{
            bottomPanelHeight: new avnav.util.Property( 50,"Bottom Height(px)",avnav.util.PropertyType.RANGE,[20,80]),
            nightMode: new avnav.util.Property( 100,"NightMode",avnav.util.PropertyType.RANGE,[0,100]),
            aisPanelWidth:new avnav.util.Property( 90,"AIS Width(px)",avnav.util.PropertyType.RANGE,[50,300]),
            routeInfoPanelWidth:new avnav.util.Property( 120,"RoutePanel Width(px)",avnav.util.PropertyType.RANGE,[50,300]),
            buttonSize:new avnav.util.Property( 60,"Button Size(px)",avnav.util.PropertyType.RANGE,[40,100]),
            centerDisplayHeight:new avnav.util.Property( 30,"+ Display Height(px)",avnav.util.PropertyType.RANGE,[20,80]),
            aisWarningColor: new avnav.util.Property( "#FA584A","Warning",avnav.util.PropertyType.COLOR),
            aisNormalColor: new avnav.util.Property( "#EBEB55","Normal",avnav.util.PropertyType.COLOR),
            aisNearestColor: new avnav.util.Property( '#70F3AF',"Nearest",avnav.util.PropertyType.COLOR),
            routeApproachingColor: new avnav.util.Property( '#FA584A',"Approach",avnav.util.PropertyType.COLOR)
        }
    }
};

function log(txt){
    try{
        //console.log(txt);
    }catch(e){}
}

function getParam(key)
{
    // Find the key and everything up to the ampersand delimiter
    var value=RegExp(""+key+"[^&]+").exec(window.location.search);

    // Return the unescaped value minus everything starting from the equals sign or an empty string
    return unescape(!!value ? value.toString().replace(/^[^=]+./,"") : "");
}

/**
 * main function called when dom is loaded
 *
 */
avnav.main=function() {
    var propertyHandler=new avnav.util.PropertyHandler(propertyDefinitions(),{});
    propertyHandler.loadUserData();
    var navobject=new avnav.nav.NavObject(propertyHandler);
    var mapholder=new avnav.map.MapHolder(propertyHandler,navobject);
    var gui=new avnav.gui.Handler(propertyHandler,navobject,mapholder);
    if (avnav_version !== undefined){
        $('#avi_mainpage_version').text(avnav_version);
    }
    var navurl=getParam('navurl');
    if (navurl){
        propertyHandler.setValueByName('navUrl',navurl);
        propertyHandler.setValueByName('routingServerError',false);
    }
    gui.showPage("mainpage");
    log("avnav loaded");
};

<commit_msg>call less.modifyVars after load as workaround for IOS issues<commit_after>/*
# vim: ts=2 sw=2 et
###############################################################################
# Copyright (c) 2014, Andreas Vogel andreas@wellenvogel.net
# parts of software from movable-type
# http://www.movable-type.co.uk/
# for their license see the file latlon.js
#
#  Permission is hereby granted, free of charge, to any person obtaining a
#  copy of this software and associated documentation files (the "Software"),
#  to deal in the Software without restriction, including without limitation
#  the rights to use, copy, modify, merge, publish, distribute, sublicense,
#  and/or sell copies of the Software, and to permit persons to whom the
#  Software is furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included
#  in all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
#  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
#  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#  DEALINGS IN THE SOFTWARE.
###############################################################################

icons partly from http://www.tutorial9.net/downloads/108-mono-icons-huge-set-of-minimal-icons/
                  http://ionicons.com/ (MIT license)
*/

avnav.provide('avnav.main');

/**
 * currently we must require all pages somewhere
 */

var propertyDefinitions=function(){
    return {
        layers:{
            ais: new avnav.util.Property(true,"AIS",avnav.util.PropertyType.CHECKBOX),
            track: new avnav.util.Property(true,"Track",avnav.util.PropertyType.CHECKBOX),
            nav: new avnav.util.Property(true,"Navigation",avnav.util.PropertyType.CHECKBOX),
            boat: new avnav.util.Property(true,"Boat",avnav.util.PropertyType.CHECKBOX),
            grid: new avnav.util.Property(true,"Grid",avnav.util.PropertyType.CHECKBOX),
            compass: new avnav.util.Property(true,"Compass",avnav.util.PropertyType.CHECKBOX)
        },
        forceMobile: new avnav.util.Property(false,"forceMobile",avnav.util.PropertyType.CHECKBOX),
        connectedMode:new avnav.util.Property(true,"connected",avnav.util.PropertyType.CHECKBOX),
        NM: new avnav.util.Property(1852), //one mile
        buttonUpdateTime: new avnav.util.Property( 500), //timer for button updates
        slideTime: new avnav.util.Property( 300), //time in ms for upzoom
        slideLevels: new avnav.util.Property( 3), //start with that many lower zoom levels
        maxUpscale: new avnav.util.Property(2), //2 levels upscale (otherwise we need too much mem)
        hideLower: new avnav.util.Property( true), //if set, hide smaller zoom layers when completely covered
        maxZoom: new avnav.util.Property( 21),  //only allow upscaling up to this zom level
        courseAverageFactor: new avnav.util.Property(0.5), //moving average for course up
        courseAverageTolerance: new avnav.util.Property(15,"Rotation Tolerance",avnav.util.PropertyType.RANGE,[1,30]), //tolerance for slow rotation
        minGridLedvel: new avnav.util.Property( 10),
        loggingEnabled: new avnav.util.Property( true),
        positionQueryTimeout: new avnav.util.Property( 1000,"Position",avnav.util.PropertyType.RANGE,[500,5000,10]), //1000ms
        trackQueryTimeout: new avnav.util.Property( 5000,"Track",avnav.util.PropertyType.RANGE,[500,10000,10]), //5s in ms
        routeQueryTimeout: new avnav.util.Property( 1000,"Route",avnav.util.PropertyType.RANGE,[500,10000,10]), //5s in ms
        bearingColor: new avnav.util.Property( "#DDA01F","Color",avnav.util.PropertyType.COLOR),
        bearingWidth: new avnav.util.Property( 3,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        routeColor: new avnav.util.Property( "#27413B","Color",avnav.util.PropertyType.COLOR),
        routeWidth: new avnav.util.Property( 2,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        routeWpSize:new avnav.util.Property( 7,"WPSize",avnav.util.PropertyType.RANGE,[5,30]),
        routeApproach: new avnav.util.Property( 200,"Approach(m)",avnav.util.PropertyType.RANGE,[20,2000]),
        routeShowLL: new avnav.util.Property(false,"showLatLon",avnav.util.PropertyType.CHECKBOX), //show latlon or leg course/len
        navCircleColor: new avnav.util.Property( "#D71038","Circle Color",avnav.util.PropertyType.COLOR),
        navCircleWidth: new avnav.util.Property( 1,"Circle Width",avnav.util.PropertyType.RANGE,[1,10]),
        navCircle1Radius: new avnav.util.Property( 300,"Circle 1 Radius(m)",avnav.util.PropertyType.RANGE,[0,5000,10]),
        navCircle2Radius: new avnav.util.Property( 1000,"Circle 2 Radius(m)",avnav.util.PropertyType.RANGE,[0,5000,10]),
        navCircle3Radius: new avnav.util.Property( 0,"Circle 3 Radius(m)",avnav.util.PropertyType.RANGE,[0,10000,10]),
        gpsXteMax:new avnav.util.Property( 1,"XTE(nm)",avnav.util.PropertyType.RANGE,[0.1,5,0.1,1]),
        trackColor: new avnav.util.Property( "#942eba","Color",avnav.util.PropertyType.COLOR),
        trackWidth: new avnav.util.Property( 3,"Width",avnav.util.PropertyType.RANGE,[1,10]),
        trackInterval: new avnav.util.Property( 30,"Point Dist.(s)",avnav.util.PropertyType.RANGE,[5,300]), //seconds
        initialTrackLength: new avnav.util.Property( 24,"Length(h)",avnav.util.PropertyType.RANGE,[1,48]), //in h
        aisQueryTimeout: new avnav.util.Property( 5000,"AIS",avnav.util.PropertyType.RANGE,[1000,10000,10]), //ms
        aisDistance: new avnav.util.Property( 20,"AIS-Range(nm)",avnav.util.PropertyType.RANGE,[1,100]), //distance for AIS query in nm
        aisClickTolerance: new avnav.util.Property( 80,"Click Tolerance",avnav.util.PropertyType.RANGE,[10,100]),
        maxAisErrors: new avnav.util.Property( 3), //after that many errors AIS display will be switched off
        minAISspeed: new avnav.util.Property( 0.2), //minimal speed in kn that we consider when computing cpa/tcpa
        maxAisTPA: new avnav.util.Property( 3),    //max. computed AIS TPA time in h (otherwise we do not consider this)
        aisWarningCpa: new avnav.util.Property( 500,"AIS Warning-CPA(m)",avnav.util.PropertyType.RANGE,[100,5000,10]), //m for AIS warning (500m)
        aisWarningTpa: new avnav.util.Property( 900,"AIS-Warning-TPA(s)",avnav.util.PropertyType.RANGE,[30,3600,10]), //in s - max time for tpa warning (15min)
        aisTextSize:new avnav.util.Property( 14,"Text Size(px)",avnav.util.PropertyType.RANGE,[8,24]), //in px
        //images are not used any more, just keeping for fallback
        aisNormalImage: new avnav.util.Property( 'images/ais-default.png'),
        aisNearestImage: new avnav.util.Property( 'images/ais-nearest.png'),
        aisWarningImage: new avnav.util.Property( 'images/ais-warning.png'),
        statusQueryTimeout: new avnav.util.Property( 3000), //ms
        centerDisplayTimeout: new avnav.util.Property( 45000), //ms - auto hide measure display (0 - no auto hide)
        navUrl: new avnav.util.Property( "/viewer/avnav_navi.php"),
        maxGpsErrors: new avnav.util.Property( 3), //after that much invalid responses/timeouts the GPS is dead
        settingsName: new avnav.util.Property( "avnav.settings"), //storage name
        routingDataName: new avnav.util.Property( "avnav.routing"),
        routeName: new avnav.util.Property( "avnav.defaultRoute"),
        routingServerError: new avnav.util.Property(true,"ServerError",avnav.util.PropertyType.CHECKBOX), //notify comm errors to server
        routingTextSize:new avnav.util.Property( 14,"Text Size(px)",avnav.util.PropertyType.RANGE,[8,36]), //in px
        centerName: new avnav.util.Property( "avnav.center"),
        statusErrorImage: new avnav.util.Property( "images/RedBubble40.png"),
        statusOkImage: new avnav.util.Property( "images/GreenBubble40.png"),
        statusIcons: {
            INACTIVE: new avnav.util.Property( "images/GreyBubble40.png"),
            STARTED: new avnav.util.Property(  "images/YellowBubble40.png"),
            RUNNING: new avnav.util.Property(  "images/YellowBubble40.png"),
            NMEA: new avnav.util.Property(	  "images/GreenBubble40.png"),
            ERROR: new avnav.util.Property(	  "images/RedBubble40.png")
        },
        nightFade:new avnav.util.Property( 15,"NightDim(%)",avnav.util.PropertyType.RANGE,[1,99]), //in px
        nightChartFade:new avnav.util.Property( 30,"NightChartDim(%)",avnav.util.PropertyType.RANGE,[1,99]), //in %
        //all style members map to less variables
        style:{
            bottomPanelHeight: new avnav.util.Property( 50,"Bottom Height(px)",avnav.util.PropertyType.RANGE,[20,80]),
            nightMode: new avnav.util.Property( 100,"NightMode",avnav.util.PropertyType.RANGE,[0,100]),
            aisPanelWidth:new avnav.util.Property( 90,"AIS Width(px)",avnav.util.PropertyType.RANGE,[50,300]),
            routeInfoPanelWidth:new avnav.util.Property( 120,"RoutePanel Width(px)",avnav.util.PropertyType.RANGE,[50,300]),
            buttonSize:new avnav.util.Property( 60,"Button Size(px)",avnav.util.PropertyType.RANGE,[40,100]),
            centerDisplayHeight:new avnav.util.Property( 30,"+ Display Height(px)",avnav.util.PropertyType.RANGE,[20,80]),
            aisWarningColor: new avnav.util.Property( "#FA584A","Warning",avnav.util.PropertyType.COLOR),
            aisNormalColor: new avnav.util.Property( "#EBEB55","Normal",avnav.util.PropertyType.COLOR),
            aisNearestColor: new avnav.util.Property( '#70F3AF',"Nearest",avnav.util.PropertyType.COLOR),
            routeApproachingColor: new avnav.util.Property( '#FA584A',"Approach",avnav.util.PropertyType.COLOR)
        }
    }
};

function log(txt){
    try{
        //console.log(txt);
    }catch(e){}
}

function getParam(key)
{
    // Find the key and everything up to the ampersand delimiter
    var value=RegExp(""+key+"[^&]+").exec(window.location.search);

    // Return the unescaped value minus everything starting from the equals sign or an empty string
    return unescape(!!value ? value.toString().replace(/^[^=]+./,"") : "");
}

/**
 * main function called when dom is loaded
 *
 */
avnav.main=function() {
    less.modifyVars();
    $("body").show();
    var propertyHandler=new avnav.util.PropertyHandler(propertyDefinitions(),{});
    propertyHandler.loadUserData();
    var navobject=new avnav.nav.NavObject(propertyHandler);
    var mapholder=new avnav.map.MapHolder(propertyHandler,navobject);
    var gui=new avnav.gui.Handler(propertyHandler,navobject,mapholder);
    if (avnav_version !== undefined){
        $('#avi_mainpage_version').text(avnav_version);
    }
    var navurl=getParam('navurl');
    if (navurl){
        propertyHandler.setValueByName('navUrl',navurl);
        propertyHandler.setValueByName('routingServerError',false);
    }
    gui.showPage("mainpage");
    log("avnav loaded");
};

<|endoftext|>


<commit_before>//= require ./button_view

// A checkbox. The state of the checkbox is indicated by the isSelected property.
Flame.CheckboxView = Flame.ButtonView.extend({
    classNames: ['flame-checkbox-view'],
    isSticky: true,

    render: function(buffer) {
        buffer.push('

');
        this.renderCheckMark(buffer);
        buffer.push('');
        buffer.push(Ember.isNone(this.get('title')) ? '' : Handlebars.Utils.escapeExpression(this.get('title')));
        buffer.push('');
    },

    renderCheckMark: function(buffer) {
        var imgUrl = Flame.image('checkmark.svg');
        buffer.push('

');
    }
});
<commit_msg>tooltip support for checkbox view<commit_after>//= require ./button_view

// A checkbox. The state of the checkbox is indicated by the isSelected property.
Flame.CheckboxView = Flame.ButtonView.extend({
    classNames: ['flame-checkbox-view'],
    isSticky: true,

    render: function(buffer) {
        buffer.push('

');
        this.renderCheckMark(buffer);
        var tooltip = this.get('tooltip') || '';
        buffer.push('');
        buffer.push(Ember.isNone(this.get('title')) ? '' : Handlebars.Utils.escapeExpression(this.get('title')));
        buffer.push('');
    },

    renderCheckMark: function(buffer) {
        var imgUrl = Flame.image('checkmark.svg');
        buffer.push('

');
    }
});
<|endoftext|>


<commit_before>'use strict';

import React, {
  Component,
  ListView,
  StyleSheet,
  Text,
  Image,
  RefreshControl,
  TouchableHighlight,
  ActivityIndicatorIOS,
  View,
  NativeAppEventEmitter
} from 'react-native';

var Style = require('./Style.js');
var NetworkErrorView = require('./NetworkErrorView.js');
var Bridge = require('react-native').NativeModules.LDTReactBridge;
var ReportbackItemView = require('./ReportbackItemView.js');
var firstSectionHeaderText = "Actions I'm Doing";
var secondSectionHeaderText = "Actions I've Done";

var UserView = React.createClass({
  getInitialState: function() {
    var getSectionData = (dataBlob, sectionID) => {
      return dataBlob[sectionID];
    }
    var getRowData = (dataBlob, sectionID, rowID) => {
      return dataBlob[sectionID + ':' + rowID];
    }
    return {
      dataSource : new ListView.DataSource({
        getSectionData          : getSectionData,
        getRowData              : getRowData,
        rowHasChanged           : (row1, row2) => row1 !== row2,
        sectionHeaderHasChanged : (s1, s2) => s1 !== s2
      }),
      isRefreshing: false,
      loaded: false,
      error: null,
      // Because selfProfile can change user data, we need to store user in state.
      user: this.props.user,
    };
  },
  componentDidMount: function() {
    if (this.props.isSelfProfile) {
      this.userActivitySubscription = NativeAppEventEmitter.addListener(
        'currentUserActivity',
        (signup) => this.handleUserActivityEvent(signup),
      );
      this.userChangedSubscription = NativeAppEventEmitter.addListener(
        'currentUserChanged',
        (user) => this.handleUserChangedEvent(user),
      );
    }
    this.fetchData();
  },
  componentWillUnmount: function() {
    if (this.props.isSelfProfile) {
      this.userActivitySubscription.remove();
      this.userChangedSubscription.remove();
    }
  },
  handleUserChangedEvent: function(user) {
    console.log("handleUserChangedEvent");
    this.state.user = user;
    this.fetchData();
  },
  handleUserActivityEvent: function(campaignActivity) {
    this.fetchData();
  },
  fetchData: function() {
    this.setState({
      error: false,
      loaded: false,
    });
    var options = {
      method: 'GET',
      timeout: 30000,
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Session': this.props.sessionToken,
        'X-DS-REST-API-Key': this.props.apiKey,
      },
    };
    var url = this.props.baseUrl + 'signups?user=' + this.state.user.id;
    fetch(url, options)
      .then((response) => response.json())
      .catch((error) => this.catchError(error))
      .then((responseData) => {
        if (!responseData) {
          return;
        }
        this.loadSignups(responseData.data);
      })
      .done();
  },
  loadSignups: function(signups) {
    var dataBlob = {},
      sectionIDs = [],
      rowIDs = [],
      i;
    sectionIDs.push(0);
    dataBlob[0] = firstSectionHeaderText;
    rowIDs[0] = [];
    sectionIDs.push(1);
    dataBlob[1] = secondSectionHeaderText;
    rowIDs[1] = [];
    for (i = 0; i < signups.length; i++) {
      var signup = signups[i];
      var sectionNumber = 0;
      if (signup.reportback) {
        sectionNumber = 1;
        signup.reportbackItem = signup.reportback.reportback_items.data[0];
      }
      else {
        if (!signup.campaign_run.current) {
          continue;
        }
        if (signup.campaign.status != 'active') {
          continue;
        }
        if (signup.campaign.type != 'campaign') {
          continue;
        }
      }
      rowIDs[sectionNumber].push(signup.id);
      dataBlob[sectionNumber + ':' + signup.id] = signup;
    }
    this.setState({
      dataSource : this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
      loaded: true,
      error: null,
    });
  },
  _onRefresh: function () {
    this.setState({isRefreshing: true});
    setTimeout(() => {
      this.fetchData();
      this.setState({
        isRefreshing: false,
      });
    }, 1000);
  },
  catchError: function(error) {
    console.log("UserView.catchError");
    this.setState({
      error: error,
    });
  },
  renderLoadingView: function() {
    // @todo DRY LoadingView ReactComponent
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicatorIOS animating={this.state.animating} style={[{height: 80}]} size="small" />
        <Text style={Style.textBody}>
          Loading profile...
        </Text>
      </View>
    );
  },
  renderEmptySelfProfile: function() {
    return (
      <View>
        {this.renderHeader()}
        <View style={Style.sectionHeader}>
          <Text style={Style.sectionHeaderText}>
            {firstSectionHeaderText.toUpperCase()}
          </Text>
        </View>
        <View style={styles.noActionsContainer}>
          <Text style={Style.textHeading}>
            You haven’t started any actions yet.
          </Text>
          <Text style={[Style.textBody, {paddingTop: 8}]}>
            And you totally should! Shit is happening in the world -- find out how to do something about it.
          </Text>
        </View>
      </View>
    );
  },
  render: function() {
    if (this.state.error) {
      return (
        <NetworkErrorView
          title="Profile isn't loading right now"
          retryHandler={this.fetchData}
          errorMessage={this.state.error.message}
        />
      );
    }
    if (!this.state.loaded) {
      return this.renderLoadingView();
    }
    if (this.state.dataSource.getRowCount() == 0 && this.props.isSelfProfile) {
      return this.renderEmptySelfProfile();
    }

    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        renderHeader={this.renderHeader}
        renderSectionHeader = {this.renderSectionHeader}
        refreshControl={
          <RefreshControl
            refreshing={this.state.isRefreshing}
            onRefresh={this._onRefresh}
            tintColor="#ccc"
            colors={['#ff0000', '#00ff00', '#0000ff']}
            progressBackgroundColor="#ffff00"
          />
        }
      />
    );
  },
  renderHeader: function() {
    var avatarUri = this.state.user.photo;
    if (avatarUri.length == 0) {
      avatarUri =  'Avatar';
    }
    var headerText = null;
    if (this.state.user.country.length > 0) {
      headerText = this.state.user.country.toUpperCase();
    }
    var avatar = (
      <Image
        style={styles.avatar}
        defaultSource={{uri: 'Placeholder Image Loading'}}
        source={{uri: avatarUri}}
      />
    );
    if (this.props.isSelfProfile) {
      avatar = (
        <TouchableHighlight onPress={() => Bridge.presentAvatarAlertController()}>
          {avatar}
        </TouchableHighlight>
      );
    }
    return (
      <View>
        <Image
          style={styles.headerBackgroundImage}
          source={require('image!Gradient Background')}>
          <View style={styles.headerContainer}>
            {avatar}
             <Text style={[Style.textHeading, styles.headerText]}>
               {headerText}
             </Text>
          </View>
        </Image>
      </View>
    );
  },
  renderSectionHeader(sectionData, sectionID) {
    return (
      <View style={Style.sectionHeader}>
        <Text style={Style.sectionHeaderText}>
          {sectionData.toUpperCase()}
        </Text>
      </View>
    );
  },
  renderRow: function(rowData) {
    if (rowData.reportback) {
      return this.renderDoneRow(rowData);
    }
    else {
      return this.renderDoingRow(rowData);
    }
  },
  renderDoingRow: function(signup) {
    return (
      <TouchableHighlight onPress={() => this._onPressRow(signup)}>
        <View style={styles.row}>
          <View style={styles.contentContainer}>
            <Text style={[Style.textHeading, Style.textColorCtaBlue]}>
              {signup.campaign.title}
            </Text>
            <Text style={Style.textBody}>
              {signup.campaign.tagline}
            </Text>
          </View>
          <View style={styles.detailContainer}>
            <View style={styles.arrowContainer}>
              <Image
                style={styles.arrowImage}
                source={require('image!Arrow')}
              />
            </View>
          </View>
        </View>
      </TouchableHighlight>
    );
  },
  renderDoneRow: function(rowData) {
    return (
      <TouchableHighlight onPress={() => this._onPressRow(rowData)}>
        <View>
          <ReportbackItemView
            key={rowData.reportbackItem.id}
            reportbackItem={rowData.reportbackItem}
            reportback={rowData.reportback}
            campaign={rowData.campaign}
            user={this.props.user}
            share={this.props.isSelfProfile}
          />
        </View>
      </TouchableHighlight>
    );
  },
  _onPressRow(rowData) {
    Bridge.pushCampaign(Number(rowData.campaign.id));
  },
});

var styles = React.StyleSheet.create({
  loadingContainer: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#EEE',
  },
  noActionsContainer : {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingLeft: 33,
    paddingRight: 33,
    paddingTop: 18,
  },
  headerContainer: {
    alignItems: 'center',
    flex: 1,
    backgroundColor: 'transparent',
    paddingTop: 20,
  },
  headerBackgroundImage: {
    flex: 1,
    height: 160,
    alignItems: 'stretch',
  },
  avatar: {
    width: 100,
    height: 100,
    borderRadius: 50,
    borderColor: 'white',
    borderWidth: 2,
    alignItems: 'center',
  },
  headerText: {
    color: 'white',
    opacity: 0.50,
    flex: 1,
    textAlign: 'center',
    paddingTop: 4,
  },
  row: {
    backgroundColor: 'white',
    padding: 8,
    flex: 1,
    flexDirection: 'row',
  },
  contentContainer: {
    flex: 1,
  },
  detailContainer: {
    width: 28,
    padding: 12,
  },
  arrowContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  arrowImage: {
    width: 12,
    height: 21,
  }
});

module.exports = UserView;<commit_msg>Centers header content<commit_after>'use strict';

import React, {
  Component,
  ListView,
  StyleSheet,
  Text,
  Image,
  RefreshControl,
  TouchableHighlight,
  ActivityIndicatorIOS,
  View,
  NativeAppEventEmitter
} from 'react-native';

var Style = require('./Style.js');
var NetworkErrorView = require('./NetworkErrorView.js');
var Bridge = require('react-native').NativeModules.LDTReactBridge;
var ReportbackItemView = require('./ReportbackItemView.js');
var firstSectionHeaderText = "Actions I'm Doing";
var secondSectionHeaderText = "Actions I've Done";

var UserView = React.createClass({
  getInitialState: function() {
    var getSectionData = (dataBlob, sectionID) => {
      return dataBlob[sectionID];
    }
    var getRowData = (dataBlob, sectionID, rowID) => {
      return dataBlob[sectionID + ':' + rowID];
    }
    return {
      dataSource : new ListView.DataSource({
        getSectionData          : getSectionData,
        getRowData              : getRowData,
        rowHasChanged           : (row1, row2) => row1 !== row2,
        sectionHeaderHasChanged : (s1, s2) => s1 !== s2
      }),
      isRefreshing: false,
      loaded: false,
      error: null,
      // Because selfProfile can change user data, we need to store user in state.
      user: this.props.user,
    };
  },
  componentDidMount: function() {
    if (this.props.isSelfProfile) {
      this.userActivitySubscription = NativeAppEventEmitter.addListener(
        'currentUserActivity',
        (signup) => this.handleUserActivityEvent(signup),
      );
      this.userChangedSubscription = NativeAppEventEmitter.addListener(
        'currentUserChanged',
        (user) => this.handleUserChangedEvent(user),
      );
    }
    this.fetchData();
  },
  componentWillUnmount: function() {
    if (this.props.isSelfProfile) {
      this.userActivitySubscription.remove();
      this.userChangedSubscription.remove();
    }
  },
  handleUserChangedEvent: function(user) {
    console.log("handleUserChangedEvent");
    this.state.user = user;
    this.fetchData();
  },
  handleUserActivityEvent: function(campaignActivity) {
    this.fetchData();
  },
  fetchData: function() {
    this.setState({
      error: false,
      loaded: false,
    });
    var options = {
      method: 'GET',
      timeout: 30000,
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Session': this.props.sessionToken,
        'X-DS-REST-API-Key': this.props.apiKey,
      },
    };
    var url = this.props.baseUrl + 'signups?user=' + this.state.user.id;
    fetch(url, options)
      .then((response) => response.json())
      .catch((error) => this.catchError(error))
      .then((responseData) => {
        if (!responseData) {
          return;
        }
        this.loadSignups(responseData.data);
      })
      .done();
  },
  loadSignups: function(signups) {
    var dataBlob = {},
      sectionIDs = [],
      rowIDs = [],
      i;
    sectionIDs.push(0);
    dataBlob[0] = firstSectionHeaderText;
    rowIDs[0] = [];
    sectionIDs.push(1);
    dataBlob[1] = secondSectionHeaderText;
    rowIDs[1] = [];
    for (i = 0; i < signups.length; i++) {
      var signup = signups[i];
      var sectionNumber = 0;
      if (signup.reportback) {
        sectionNumber = 1;
        signup.reportbackItem = signup.reportback.reportback_items.data[0];
      }
      else {
        if (!signup.campaign_run.current) {
          continue;
        }
        if (signup.campaign.status != 'active') {
          continue;
        }
        if (signup.campaign.type != 'campaign') {
          continue;
        }
      }
      rowIDs[sectionNumber].push(signup.id);
      dataBlob[sectionNumber + ':' + signup.id] = signup;
    }
    this.setState({
      dataSource : this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
      loaded: true,
      error: null,
    });
  },
  _onRefresh: function () {
    this.setState({isRefreshing: true});
    setTimeout(() => {
      this.fetchData();
      this.setState({
        isRefreshing: false,
      });
    }, 1000);
  },
  catchError: function(error) {
    console.log("UserView.catchError");
    this.setState({
      error: error,
    });
  },
  renderLoadingView: function() {
    // @todo DRY LoadingView ReactComponent
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicatorIOS animating={this.state.animating} style={[{height: 80}]} size="small" />
        <Text style={Style.textBody}>
          Loading profile...
        </Text>
      </View>
    );
  },
  renderEmptySelfProfile: function() {
    return (
      <View>
        {this.renderHeader()}
        <View style={Style.sectionHeader}>
          <Text style={Style.sectionHeaderText}>
            {firstSectionHeaderText.toUpperCase()}
          </Text>
        </View>
        <View style={styles.noActionsContainer}>
          <Text style={Style.textHeading}>
            You haven’t started any actions yet.
          </Text>
          <Text style={[Style.textBody, {paddingTop: 8}]}>
            And you totally should! Shit is happening in the world -- find out how to do something about it.
          </Text>
        </View>
      </View>
    );
  },
  render: function() {
    if (this.state.error) {
      return (
        <NetworkErrorView
          title="Profile isn't loading right now"
          retryHandler={this.fetchData}
          errorMessage={this.state.error.message}
        />
      );
    }
    if (!this.state.loaded) {
      return this.renderLoadingView();
    }
    if (this.state.dataSource.getRowCount() == 0 && this.props.isSelfProfile) {
      return this.renderEmptySelfProfile();
    }

    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        renderHeader={this.renderHeader}
        renderSectionHeader = {this.renderSectionHeader}
        refreshControl={
          <RefreshControl
            refreshing={this.state.isRefreshing}
            onRefresh={this._onRefresh}
            tintColor="#ccc"
            colors={['#ff0000', '#00ff00', '#0000ff']}
            progressBackgroundColor="#ffff00"
          />
        }
      />
    );
  },
  renderHeader: function() {
    var avatarUri = this.state.user.photo;
    if (avatarUri.length == 0) {
      avatarUri =  'Avatar';
    }
    var headerText = null;
    if (this.state.user.country.length > 0) {
      headerText = this.state.user.country.toUpperCase();
    }
    var avatar = (
      <Image
        style={styles.avatar}
        defaultSource={{uri: 'Placeholder Image Loading'}}
        source={{uri: avatarUri}}
      />
    );
    if (this.props.isSelfProfile) {
      avatar = (
        <TouchableHighlight onPress={() => Bridge.presentAvatarAlertController()}>
          {avatar}
        </TouchableHighlight>
      );
    }
    return (
      <View style={styles.headerContainer}>
        <Image
          style={styles.headerBackgroundImage}
          source={require('image!Gradient Background')}>
          <View style={styles.avatarContainer}>
            {avatar}
            <Text style={[Style.textHeading, styles.headerText]}>
              {headerText}
            </Text>
          </View>

        </Image>
      </View>
    );
  },
  renderSectionHeader(sectionData, sectionID) {
    return (
      <View style={Style.sectionHeader}>
        <Text style={Style.sectionHeaderText}>
          {sectionData.toUpperCase()}
        </Text>
      </View>
    );
  },
  renderRow: function(rowData) {
    if (rowData.reportback) {
      return this.renderDoneRow(rowData);
    }
    else {
      return this.renderDoingRow(rowData);
    }
  },
  renderDoingRow: function(signup) {
    return (
      <TouchableHighlight onPress={() => this._onPressRow(signup)}>
        <View style={styles.row}>
          <View style={styles.contentContainer}>
            <Text style={[Style.textHeading, Style.textColorCtaBlue]}>
              {signup.campaign.title}
            </Text>
            <Text style={Style.textBody}>
              {signup.campaign.tagline}
            </Text>
          </View>
          <View style={styles.detailContainer}>
            <View style={styles.arrowContainer}>
              <Image
                style={styles.arrowImage}
                source={require('image!Arrow')}
              />
            </View>
          </View>
        </View>
      </TouchableHighlight>
    );
  },
  renderDoneRow: function(rowData) {
    return (
      <TouchableHighlight onPress={() => this._onPressRow(rowData)}>
        <View>
          <ReportbackItemView
            key={rowData.reportbackItem.id}
            reportbackItem={rowData.reportbackItem}
            reportback={rowData.reportback}
            campaign={rowData.campaign}
            user={this.props.user}
            share={this.props.isSelfProfile}
          />
        </View>
      </TouchableHighlight>
    );
  },
  _onPressRow(rowData) {
    Bridge.pushCampaign(Number(rowData.campaign.id));
  },
});

var styles = React.StyleSheet.create({
  loadingContainer: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#EEE',
  },
  noActionsContainer : {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingLeft: 33,
    paddingRight: 33,
    paddingTop: 18,
  },
  headerContainer: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  avatarContainer: {
    paddingTop: 20,
    alignItems: 'center',
  },
  headerBackgroundImage: {
    height: 160,
  },
  avatar: {
    width: 100,
    height: 100,
    borderRadius: 50,
    borderColor: 'white',
    borderWidth: 2,
  },
  headerText: {
    color: 'white',
    opacity: 0.50,
    paddingTop: 4,
    alignItems: 'center',
    backgroundColor: 'transparent',
  },
  row: {
    backgroundColor: 'white',
    padding: 8,
    flex: 1,
    flexDirection: 'row',
  },
  contentContainer: {
    flex: 1,
  },
  detailContainer: {
    width: 28,
    padding: 12,
  },
  arrowContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  arrowImage: {
    width: 12,
    height: 21,
  }
});

module.exports = UserView;<|endoftext|>


<commit_before>var http = require('http');
var fs = require('fs');
var urllib = require("url");
var Client = require('node-ssdp').Client;
var dgram = require('dgram');

//null will cause the server to discover the Roku on startup, hard coding a value will allow for faster startups
// When manually setting this, include the protocol, port, and trailing slash eg:
// var rokuAddress = "http://192.168.1.100:8060/";
var rokuAddress = null;
var PORT=1234;

var ssdp = new Client();

//handle the ssdp response when the roku is found
ssdp.on('response', function (headers, statusCode, rinfo) {
	rokuAddress = headers.LOCATION;
	console.log("Found Roku: ",rokuAddress);
});

//this is called periodically and will only look for the roku if we don't already have an address
function searchForRoku() {
	if (rokuAddress == null) {
		ssdp.search('roku:ecp');
	}
}

//a simple wrapper to post to a url with no payload (to send roku commands)
function post(url,callback) {
	var info = urllib.parse(url);
	console.log("Posting: ",url);
    var opt = {
        host:info.hostname,
		port:info.port,
        path: info.path,
        method: 'POST',
    };

    var req = http.request(opt, callback);

    req.end();
}

//Performing an operation on the roku normally takes a handful of button presses
//This function will perform the list of commands in order and if a numerical value is included in the sequence it will be inserted as a delay
function postSequence(sequence,callback) {
	function handler() {
		if (sequence.length == 0) {
			if (callback) callback();
			return;
		}
		var next = sequence.shift();
		if (typeof next === "number") {
			setTimeout(handler,next);
		} else if (typeof next === "string") {
			post(next,function(res) {
                res.on("data",function() {}); //required for the request to go through without error
                handler();
			});
		}
	}
	handler();
}

//In order to send keyboard input to the roku, we use the keyress/Lit_* endpoint which can be any alphanumeric character
//This function turns a string into a series of these commands with delays of 100ms built in
//NOTE: this currently ignores anything that isn't lowercase alpha
function createTypeSequence(text) {
	var sequence = [];
	for (var i=0; i<text.length; i++) {
		var c = text.charCodeAt(i);
		if (c >= 97 && c <=122) { //alpha only
			sequence.push(rokuAddress+"keypress/Lit_"+text.charAt(i));
			sequence.push(100);
		}
	}
	return sequence;
}

//simple helper function to pull the data out of a post request. This could be avoided by using a more capable library such
function getRequestData(request,callback) {
	var body = "";
	request.on("data",function(data) {
		body += String(data);
	});
	request.on("end",function() {
		callback(body);
	});
}

//depending on the URL endpoint accessed, we use a different handler.
//This is almost certainly not the optimal way to build a TCP server, but for our simple example, it is more than sufficient
var handlers = {
    //This will play the last searched movie or show, we use it because it consistently resides to the right of the search box
	"/roku/playlast":function(request,response) {
		postSequence([
			rokuAddress+"keypress/home",    //wake the roku up, if its not already
			rokuAddress+"keypress/home",    //go back to the home screen (even if we're in netflix, we need to reset the interface)
			3000,                           //loading the home screen takes a few seconds
			rokuAddress+"launch/12",        //launch the netflix channel (presumably this is always id 12..)
			7000,                           //loading netflix also takes some time
			rokuAddress+"keypress/down",    //the last searched item is always one click down and one click to the right of where the cursor starts
			rokuAddress+"keypress/right",
			1000,                           //more delays, experimentally tweaked.. can probably be significantly reduced by more tweaking
			rokuAddress+"keypress/Select",  //select the show from the main menu
			3000,                           //give the show splash screen time to load up
			rokuAddress+"keypress/Play"     //play the current/next episode (whichever one comes up by default)
		]);
		response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays
	},
    //This endpoint doenst perform any operations, but it allows an easy way for you to dictate typed text without having to use the on screen keyboard
	"/roku/type":function(request,response) {
		getRequestData(request,function(data) {
			var text = data.replace(/^\s+|\s+$/g,'').toLowerCase(); //trim whitespace and lowercase
			var sequence = createTypeSequence(text);
			postSequence(sequence,function() {

			});
			response.end("OK");
		});
	},
    //Takes the POST data and uses it to search for a show and then immediate plays that show
	"/roku/searchplay":function(request,response) {
		getRequestData(request,function(data) {
			var text = data.replace(/^\s+|\s+$/g,'').toLowerCase(); //trim whitespace and lowercase
			var sequence = [].concat([
				rokuAddress+"keypress/home",    //wake roku
				rokuAddress+"keypress/home",    //reset to home screen
				3000,
				rokuAddress+"launch/12",        //launch netflix app
				7000,
				rokuAddress+"keypress/down",    //navigate to search
				1000,
				rokuAddress+"keypress/Select",  //select search
				2000,
			],createTypeSequence(text),[        //enter the text
				1000,
				rokuAddress+"keypress/right",   //go to search selections (which show up to the right of they keyboard.. we need to tap through them)
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				500,
				rokuAddress+"keypress/Select", //selected the top result and returns to the main screen
				3000,                          //wait for main menu
				rokuAddress+"keypress/right",  //goto searched item
				rokuAddress+"keypress/Select", //drill into show
				3000,
				rokuAddress+"keypress/Play",   //play when loaded
			]);
			postSequence(sequence);
			response.end("OK");	 //respond with OK before the operation finishes
		});
	},
    //the play and pause buttons are the same and is called "Play"
	"/roku/playpause":function(request,response) {
		post(rokuAddress+"keypress/Play");
		response.end("OK");
	},
	"/roku/nextepisode":function(request,response) {
		postSequence([
			rokuAddress+"keypress/back",
			1000,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/select",
			2000,
			rokuAddress+"keypress/right",
			100,
			rokuAddress+"keypress/select",
			1000,
			rokuAddress+"keypress/Play",
		],function() {

		});
		response.end("OK");
	},
	"/roku/lastepisode":function(request,response) {
		postSequence([
			rokuAddress+"keypress/back",
			1000,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/select",
			2000,
			rokuAddress+"keypress/left",
			100,
			rokuAddress+"keypress/select",
			1000,
			rokuAddress+"keypress/Play",
		],function() {

		});
		response.end("OK");
	}
}

//handles and incoming request by calling the appropriate handler based on the URL
function handleRequest(request, response){
	if (handlers[request.url]) {
		handlers[request.url](request,response);
	} else {
		console.log("Unknown request URL: ",request.url);
		response.end();
	}
}
//start the MSEARCH background task to try every second (run it immediately too)
setInterval(searchForRoku,1000);
searchForRoku();

//start the tcp server
http.createServer(handleRequest).listen(PORT,function(){
    console.log("Server listening on: http://localhost:%s", PORT);
});
<commit_msg>reverting change<commit_after>var http = require('http');
var fs = require('fs');
var urllib = require("url");
var Client = require('node-ssdp').Client;
var dgram = require('dgram');

//null will cause the server to discover the Roku on startup, hard coding a value will allow for faster startups
var rokuAddress = null;
var PORT=1234;

var ssdp = new Client();

//handle the ssdp response when the roku is found
ssdp.on('response', function (headers, statusCode, rinfo) {
	rokuAddress = headers.LOCATION;
	console.log("Found Roku: ",rokuAddress);
});

//this is called periodically and will only look for the roku if we don't already have an address
function searchForRoku() {
	if (rokuAddress == null) {
		ssdp.search('roku:ecp');
	}
}

//a simple wrapper to post to a url with no payload (to send roku commands)
function post(url,callback) {
	var info = urllib.parse(url);
	console.log("Posting: ",url);
    var opt = {
        host:info.hostname,
		port:info.port,
        path: info.path,
        method: 'POST',
    };

    var req = http.request(opt, callback);

    req.end();
}

//Performing an operation on the roku normally takes a handful of button presses
//This function will perform the list of commands in order and if a numerical value is included in the sequence it will be inserted as a delay
function postSequence(sequence,callback) {
	function handler() {
		if (sequence.length == 0) {
			if (callback) callback();
			return;
		}
		var next = sequence.shift();
		if (typeof next === "number") {
			setTimeout(handler,next);
		} else if (typeof next === "string") {
			post(next,function(res) {
                res.on("data",function() {}); //required for the request to go through without error
                handler();
			});
		}
	}
	handler();
}

//In order to send keyboard input to the roku, we use the keyress/Lit_* endpoint which can be any alphanumeric character
//This function turns a string into a series of these commands with delays of 100ms built in
//NOTE: this currently ignores anything that isn't lowercase alpha
function createTypeSequence(text) {
	var sequence = [];
	for (var i=0; i<text.length; i++) {
		var c = text.charCodeAt(i);
		if (c >= 97 && c <=122) { //alpha only
			sequence.push(rokuAddress+"keypress/Lit_"+text.charAt(i));
			sequence.push(100);
		}
	}
	return sequence;
}

//simple helper function to pull the data out of a post request. This could be avoided by using a more capable library such
function getRequestData(request,callback) {
	var body = "";
	request.on("data",function(data) {
		body += String(data);
	});
	request.on("end",function() {
		callback(body);
	});
}

//depending on the URL endpoint accessed, we use a different handler.
//This is almost certainly not the optimal way to build a TCP server, but for our simple example, it is more than sufficient
var handlers = {
    //This will play the last searched movie or show, we use it because it consistently resides to the right of the search box
	"/roku/playlast":function(request,response) {
		postSequence([
			rokuAddress+"keypress/home",    //wake the roku up, if its not already
			rokuAddress+"keypress/home",    //go back to the home screen (even if we're in netflix, we need to reset the interface)
			3000,                           //loading the home screen takes a few seconds
			rokuAddress+"launch/12",        //launch the netflix channel (presumably this is always id 12..)
			7000,                           //loading netflix also takes some time
			rokuAddress+"keypress/down",    //the last searched item is always one click down and one click to the right of where the cursor starts
			rokuAddress+"keypress/right",
			1000,                           //more delays, experimentally tweaked.. can probably be significantly reduced by more tweaking
			rokuAddress+"keypress/Select",  //select the show from the main menu
			3000,                           //give the show splash screen time to load up
			rokuAddress+"keypress/Play"     //play the current/next episode (whichever one comes up by default)
		]);
		response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays
	},
    //This endpoint doenst perform any operations, but it allows an easy way for you to dictate typed text without having to use the on screen keyboard
	"/roku/type":function(request,response) {
		getRequestData(request,function(data) {
			var text = data.replace(/^\s+|\s+$/g,'').toLowerCase(); //trim whitespace and lowercase
			var sequence = createTypeSequence(text);
			postSequence(sequence,function() {

			});
			response.end("OK");
		});
	},
    //Takes the POST data and uses it to search for a show and then immediate plays that show
	"/roku/searchplay":function(request,response) {
		getRequestData(request,function(data) {
			var text = data.replace(/^\s+|\s+$/g,'').toLowerCase(); //trim whitespace and lowercase
			var sequence = [].concat([
				rokuAddress+"keypress/home",    //wake roku
				rokuAddress+"keypress/home",    //reset to home screen
				3000,
				rokuAddress+"launch/12",        //launch netflix app
				7000,
				rokuAddress+"keypress/down",    //navigate to search
				1000,
				rokuAddress+"keypress/Select",  //select search
				2000,
			],createTypeSequence(text),[        //enter the text
				1000,
				rokuAddress+"keypress/right",   //go to search selections (which show up to the right of they keyboard.. we need to tap through them)
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				100,
				rokuAddress+"keypress/right",
				500,
				rokuAddress+"keypress/Select", //selected the top result and returns to the main screen
				3000,                          //wait for main menu
				rokuAddress+"keypress/right",  //goto searched item
				rokuAddress+"keypress/Select", //drill into show
				3000,
				rokuAddress+"keypress/Play",   //play when loaded
			]);
			postSequence(sequence);
			response.end("OK");	 //respond with OK before the operation finishes
		});
	},
    //the play and pause buttons are the same and is called "Play"
	"/roku/playpause":function(request,response) {
		post(rokuAddress+"keypress/Play");
		response.end("OK");
	},
	"/roku/nextepisode":function(request,response) {
		postSequence([
			rokuAddress+"keypress/back",
			1000,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/select",
			2000,
			rokuAddress+"keypress/right",
			100,
			rokuAddress+"keypress/select",
			1000,
			rokuAddress+"keypress/Play",
		],function() {

		});
		response.end("OK");
	},
	"/roku/lastepisode":function(request,response) {
		postSequence([
			rokuAddress+"keypress/back",
			1000,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/down",
			100,
			rokuAddress+"keypress/select",
			2000,
			rokuAddress+"keypress/left",
			100,
			rokuAddress+"keypress/select",
			1000,
			rokuAddress+"keypress/Play",
		],function() {

		});
		response.end("OK");
	}
}

//handles and incoming request by calling the appropriate handler based on the URL
function handleRequest(request, response){
	if (handlers[request.url]) {
		handlers[request.url](request,response);
	} else {
		console.log("Unknown request URL: ",request.url);
		response.end();
	}
}
//start the MSEARCH background task to try every second (run it immediately too)
setInterval(searchForRoku,1000);
searchForRoku();

//start the tcp server
http.createServer(handleRequest).listen(PORT,function(){
    console.log("Server listening on: http://localhost:%s", PORT);
});
<|endoftext|>