<commit_before>(function (enyo) {
    var validationWord, formValidated;
    var exceptions, validations, constrains; // Dictionaries
    var runChecker, setElementClasses, shouldValidate, validateForm, findControlByName, validateControl, keyHasError, addEventListener; // Helper functions

    formValidated = false;
    validationWord = "is";

    // Prepublished exceptions
    exceptions = {
        validation: {
            name: "InvalidValidationOperation",
            message: "The validation operation could not be found."
        },
        constrain: {
            name: "InvalidConstrainCheck",
            message: "The constrain check could not be done."
        },
        events: {
            name: "EventsNotSupported",
            message: "The environment is not supporting known event listening API."
        }
    };
    // Validation algorithms
    validations = {
        number: {
            check: function (input) {
                return input === +input;
            },
            error: " should be a number."
        },
        alphanumeric: {
            check: function (input) {
                return (/^\w+$/g).test(input);
            },
            error: " should consist of letters or numbers."
        },
        email: {
            check: function (input) {
                return (/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i).test(input); //'
            },
            error: " should be a correctly formatted email address."
        },
        required: {
            check: function (input) {
                return ( !! input && input !== 0) || input === 0;
            },
            error: " should not be empty."
        }
    };
    // Constrain check algorithms
    constrains = {
        min: {
            val: undefined,
            check: function (input) {
                this.error = " should be more than " + this.val + " characters long.";
                return input.length >= this.val;
            },
            error: ""
        },
        max: {
            val: undefined,
            check: function (input) {
                this.error = " should be less than " + this.val + " characters long.";
                return input.length <= this.val;
            },
            error: ""
        },
        gt: {
            val: undefined,
            check: function (input) {
                this.error = "  should be greater than " + this.val + ".";
                return input > this.val;
            },
            error: ""
        },
        lt: {
            val: undefined,
            check: function (input) {
                this.error = " should be less than " + this.val + ".";
                return input < this.val;
            },
            error: ""
        },
        eq: {
            val: undefined,
            check: function (input) {
                this.error = " should be equal to " + this.val + ".";
                return input === this.val;
            },
            error: ""
        },
        sameas: {
            val: undefined,
            scope: {},
            check: function (input, inputEl) {
                this.error = " should be same as " + this.val + ".";
                var control = findControlByName.call(this.scope, this.val);
                addEventListener(control.node, "change", this.scope.validate.bind(this.scope, inputEl));
                return input === control.getValue();
            }
        }
    };

    // For some fucked up reason enyo.dispatcher wont work.
    addEventListener = function(inNode, inEventType, inHandler) {
        if (inNode.addEventListener) {
            inNode.addEventListener(inEventType, inHandler, false);
        }
        else if (inNode.attachEvent) {
            inNode.attachEvent('on'+inEventType, inHandler);
        } else {
            throw exceptions.events;
        }
    };

    runChecker = function (checkFn, inputControl) {
        var key = inputControl.getName();
        // Call the `check` function of the validator and the constrains.
        // Put any possible errors in case validation fails.
        if (!checkFn.check(inputControl.getValue(), inputControl)) {
            this.errors[key] = this.errors[key] || [];
            this.errors[key].push(checkFn.error);
        }
    };

    findControlByName = function(name) {
        var foundControl;
        var controls, control, counter;
        controls = this.getControls();
        for(counter = 0; counter<controls.length; counter++ ) {
            if (foundControl) {
                return foundControl;
            }
            control = controls[counter];
            if ( control.name === name ) {
                foundControl = control;
                return foundControl;
            } else {
                foundControl = findControlByName.call(control, name);
            }
        }
        return foundControl;
    };

    keyHasError = function(key) {
        return this.errors.hasOwnProperty(key) && this.errors[key].length > 0;
    };

    setElementClasses = function (inputEl) {
        var inputName = inputEl.getName(),
        errorExists = keyHasError.call(this,inputName),
        parent = inputEl.parent;

        // If it is in a decorator, the parent class should get the styles.
        if (parent && (parent.hasClass("enyo-tool-decorator") || parent.hasClass("onyx-input-decorator"))) {
            inputEl = parent;
        }
        inputEl.addRemoveClass(this.getErrorClass(),  errorExists);
        inputEl.addRemoveClass(this.getSuccessClass(), !errorExists);
    };

    shouldValidate = function(control) {
        return control.hasOwnProperty(validationWord);
    };

    validateControl = function(control) {
        var prefferedValidations, selectedConstrain;
        prefferedValidations = control[validationWord].toLowerCase().split(" ");
        enyo.forEach(prefferedValidations, function (validationName) {
            if (validations.hasOwnProperty(validationName)) {
                runChecker.apply(this, [validations[validationName], control]);
            } else {
                throw exceptions.validation;
            }
        }, this);

        for (var constrain in constrains) {
            if (control.hasOwnProperty(constrain)) {
                selectedConstrain = constrains[constrain];
                selectedConstrain.val = control[constrain];
                selectedConstrain.scope ? selectedConstrain.scope = this : undefined;
                runChecker.apply(this, [selectedConstrain, control]);
            }
        }

        if (this.getWithClasses() === true) {
            setElementClasses.call(this, control);
        }
    };

    validateForm = function (form) {
        // Recursive check in children controls
        enyo.forEach(form.getControls(), function (control) {
            if (!shouldValidate(control)) {
                validateForm.call(this, control);
            } else {
                validateControl.call(this, control);
            }
        }, this);
        formValidated = true;
    };

    return enyo.kind({
        name: "orthos.Validatable",
        kind: enyo.Control,
        published: {
            errorClass: "validation-error",
            successClass: "validation-success",
            withClasses: true,
            live: true
        },
        events: {
            onLiveError: "",
            onLiveSuccess: ""
        },
        handlers: {
            onchange: "_handleChange"
        },
        components: [],
        errors: {},
        /**
         * Checks if the validation is passed by looking if there are any errors in the `errors` object
         * @return bool
         */
        create: function () {
            this.inherited(arguments);
        },
        rendered: function () {
            this.createComponent({
                name: "errorsPopup",
                kind: "onyx.Popup",
                floating: true,
                centered: true,
                style: "padding: 10px",
                allowHtml: true
            });
            this.inherited(arguments);
        },
        isValid: function () {
            var errorKey;
            if (!formValidated) {
                throw new Error("Form not validated");
            }
            // Checks if there are any errors in the errors object. If there are, can not be valid.
            for (errorKey in this.errors) {
                if (keyHasError.call(this,errorKey)) {
                    return false;
                }
            }
            return true;
        },
        /**
         * Returns an array containing all the errors located in the `errors` object.
         * @return array The array with the error objects
         */
        getErrorsArray: function () {
            var errorKey, errorsArray, combinedError;
            errorsArray = [];
            for (errorKey in this.errors) {
                if (keyHasError.call(this,errorKey)) {
                    combinedError = errorKey + this.errors[errorKey][0];
                    if (this.errors[errorKey].length > 1) {
                        for (var i = 1; i < this.errors[errorKey].length; i++) {
                            combinedError = combinedError.substring(0, combinedError.length - 1).replace(" and", ",");
                            combinedError += " and " + this.errors[errorKey][i];
                        }
                    }

                    errorsArray.push(enyo.cap(combinedError));
                }
            }
            return errorsArray;
        },
        /**
         * Returns a formated HTML list of the errors that the `errors` object contains.
         * @return string
         */
        getErrorsHtml: function () {
            var errorsArray, errorsHtml;
            errorsHtml = "";
            errorsArray = this.getErrorsArray();
            if (errorsArray.length !== 0) {
                errorsHtml = "

";
                errorsHtml += errorsArray.join("

");
                errorsHtml += "

";
            }
            return errorsHtml;
        },
        /**
         * Shows a pop up formatted with HTML with all the errors of the `errors` object in it
         * @return void
         */
        showErrorsPopup: function () {
            if (!this.isValid()) {
                this.$.errorsPopup.setContent(this.getErrorsHtml());
                this.$.errorsPopup.show();
            }
        },
        validate: function (control) {
            control = control || this;
            if ( control === this ) {
                // this reffers to the present control which is the container of all inputs - form
                this.errors = {};
                validateForm.call(this, this);
            } else {
                // when specific control is given as an argument
                this.errors = this.errors || {};
                this.errors[control.name] = [];
                validateControl.call(this, control);
            }
        },
        _handleChange: function(inSender, inEvent) {
            var control = inEvent.originator;
            if( this.getLive() && shouldValidate(control) ){
                this.validate(control);
                if( keyHasError.call(this,control.name) ){
                    this.doLiveError(control);
                } else {
                    this.doLiveSuccess(control);
                }
            }
        }
    });
})(enyo);
<commit_msg>Added validation for alphabetical characters only. Custom validation functionality<commit_after>(function (enyo) {
    var validationWord, formValidated;
    var exceptions, validations, constrains; // Dictionaries
    var runChecker, setElementClasses, shouldValidate, validateForm, findControlByName, validateControl, keyHasError, addEventListener; // Helper functions

    formValidated = false;
    validationWord = "is";

    // Prepublished exceptions
    exceptions = {
        validation: {
            name: "InvalidValidationOperation",
            message: "The validation operation could not be found."
        },
        constrain: {
            name: "InvalidConstrainCheck",
            message: "The constrain check could not be done."
        },
        events: {
            name: "EventsNotSupported",
            message: "The environment is not supporting known event listening API."
        }
    };
    // Validation algorithms
    validations = {
        number: {
            check: function (input) {
                return input === +input;
            },
            error: "should be a number."
        },
        alphanumeric: {
            check: function (input) {
                return (/^\w+$/g).test(input);
            },
            error: "should consist of letters or numbers."
        },
        alphabetical: {
            check: function(input) {
                return(/^[A-Za-z]+$/g).test(input);
            },
            error: "should consist of alphabetical characters only."
        },
        email: {
            check: function (input) {
                return (/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i).test(input); //'
            },
            error: "should be a correctly formatted email address."
        },
        required: {
            check: function (input) {
                return ( !! input && input !== 0) || input === 0;
            },
            error: "should not be empty."
        }
    };
    // Constrain check algorithms
    constrains = {
        min: {
            val: undefined,
            check: function (input) {
                this.error = "should be more than " + this.val + " characters long.";
                return input.length >= this.val;
            },
            error: ""
        },
        max: {
            val: undefined,
            check: function (input) {
                this.error = "should be less than " + this.val + " characters long.";
                return input.length <= this.val;
            },
            error: ""
        },
        gt: {
            val: undefined,
            check: function (input) {
                this.error = "should be greater than " + this.val + ".";
                return input > this.val;
            },
            error: ""
        },
        lt: {
            val: undefined,
            check: function (input) {
                this.error = "should be less than " + this.val + ".";
                return input < this.val;
            },
            error: ""
        },
        eq: {
            val: undefined,
            check: function (input) {
                this.error = "should be equal to " + this.val + ".";
                return input === this.val;
            },
            error: ""
        },
        sameas: {
            val: undefined,
            scope: {},
            check: function (input, inputEl) {
                this.error = "should be same as " + this.val + ".";
                var control = findControlByName.call(this.scope, this.val);
                addEventListener(control.node, "change", this.scope.validate.bind(this.scope, inputEl));
                return input === control.getValue();
            }
        }
    };

    // For some fucked up reason enyo.dispatcher wont work.
    addEventListener = function(inNode, inEventType, inHandler) {
        if (inNode.addEventListener) {
            inNode.addEventListener(inEventType, inHandler, false);
        }
        else if (inNode.attachEvent) {
            inNode.attachEvent('on'+inEventType, inHandler);
        } else {
            throw exceptions.events;
        }
    };

    runChecker = function (checkFn, inputControl) {
        var key = inputControl.getName();
        // Call the `check` function of the validator and the constrains.
        // Put any possible errors in case validation fails.
        if (!checkFn.check(inputControl.getValue(), inputControl)) {
            this.errors[key] = this.errors[key] || [];
            this.errors[key].push(checkFn.error);
        }
    };

    findControlByName = function(name) {
        var foundControl;
        var controls, control, counter;
        controls = this.getControls();
        for(counter = 0; counter<controls.length; counter++ ) {
            if (foundControl) {
                return foundControl;
            }
            control = controls[counter];
            if ( control.name === name ) {
                foundControl = control;
                return foundControl;
            } else {
                foundControl = findControlByName.call(control, name);
            }
        }
        return foundControl;
    };

    keyHasError = function(key) {
        return this.errors.hasOwnProperty(key) && this.errors[key].length > 0;
    };

    setElementClasses = function (inputEl) {
        var inputName = inputEl.getName(),
        errorExists = keyHasError.call(this,inputName),
        parent = inputEl.parent;

        // If it is in a decorator, the parent class should get the styles.
        if (parent && (parent.hasClass("enyo-tool-decorator") || parent.hasClass("onyx-input-decorator"))) {
            inputEl = parent;
        }
        inputEl.addRemoveClass(this.getErrorClass(),  errorExists);
        inputEl.addRemoveClass(this.getSuccessClass(), !errorExists);
    };

    shouldValidate = function(control) {
        return control.hasOwnProperty(validationWord);
    };

    validateControl = function(control) {
        var prefferedValidations, selectedConstrain;
        prefferedValidations = control[validationWord].toLowerCase().split(" ");
        enyo.forEach(prefferedValidations, function (validationName) {
            if (validations.hasOwnProperty(validationName)) {
                runChecker.apply(this, [validations[validationName], control]);
            } else {
                throw exceptions.validation;
            }
        }, this);

        for (var constrain in constrains) {
            if (control.hasOwnProperty(constrain)) {
                selectedConstrain = constrains[constrain];
                selectedConstrain.val = control[constrain];
                selectedConstrain.scope ? selectedConstrain.scope = this : undefined;
                runChecker.apply(this, [selectedConstrain, control]);
            }
        }

        if (this.getWithClasses() === true) {
            setElementClasses.call(this, control);
        }
    };

    validateForm = function (form) {
        // Recursive check in children controls
        enyo.forEach(form.getControls(), function (control) {
            if (!shouldValidate(control)) {
                validateForm.call(this, control);
            } else {
                validateControl.call(this, control);
            }
        }, this);
        formValidated = true;
    };

    return enyo.kind({
        name: "orthos.Validatable",
        kind: enyo.Control,
        published: {
            errorClass: "validation-error",
            successClass: "validation-success",
            withClasses: true,
            live: true
        },
        events: {
            onLiveError: "",
            onLiveSuccess: ""
        },
        handlers: {
            onchange: "_handleChange"
        },
        components: [],
        errors: {},
        statics: {
            /**
             * Adds a custom validation method.
             * @param {String} alias      The alias of the specific validation. e.g. `is:"<alias>"`.
             * @param {(Function|String)} validation The validation function or regexp which will be used to validate the input.
             * @param {String} errorMsg   The error message which  will appear in case of invalid input to the user.
             * @param {Boolean} override  Override existing validation with the same alias.
             */
            addValidation: function(alias, validation, errorMsg, override) {
                var validateFn;
                if (validation instanceof RegExp) {
                    validateFn = function(input) {
                        return validation.test(input);
                    };
                } else if( validation instanceof Function) {
                    validateFn = validation;
                } else {
                    validateFn = function() {
                        return true;
                    };
                    enyo.warn("No validation Function or RegExp passed.");
                }
                if (validations && alias && (!validations[alias] || override) ) {
                    alias = enyo.toLowerCase(alias);
                    validations[alias] = {
                        check: validateFn,
                        error: errorMsg
                    };
                } else {
                    enyo.log("Validation alias already exists.Change alias or set override to true.");
                }
            }
        },
        /**
         * Checks if the validation is passed by looking if there are any errors in the `errors` object
         * @return bool
         */
        create: function () {
            this.inherited(arguments);
        },
        rendered: function () {
            this.createComponent({
                name: "errorsPopup",
                kind: "onyx.Popup",
                floating: true,
                centered: true,
                style: "padding: 10px",
                allowHtml: true
            });
            this.inherited(arguments);
        },
        isValid: function () {
            var errorKey;
            if (!formValidated) {
                throw new Error("Form not validated");
            }
            // Checks if there are any errors in the errors object. If there are, can not be valid.
            for (errorKey in this.errors) {
                if (keyHasError.call(this,errorKey)) {
                    return false;
                }
            }
            return true;
        },
        /**
         * Returns an array containing all the errors located in the `errors` object.
         * @return array The array with the error objects
         */
        getErrorsArray: function () {
            var errorKey, errorsArray, combinedError;
            errorsArray = [];
            for (errorKey in this.errors) {
                if (keyHasError.call(this,errorKey)) {
                    combinedError = errorKey + " " + this.errors[errorKey][0];
                    if (this.errors[errorKey].length > 1) {
                        for (var i = 1; i < this.errors[errorKey].length; i++) {
                            combinedError = combinedError.substring(0, combinedError.length - 1).replace(" and", ",");
                            combinedError += " and " + this.errors[errorKey][i];
                        }
                    }

                    errorsArray.push(enyo.cap(combinedError));
                }
            }
            return errorsArray;
        },
        /**
         * Returns a formated HTML list of the errors that the `errors` object contains.
         * @return string
         */
        getErrorsHtml: function () {
            var errorsArray, errorsHtml;
            errorsHtml = "";
            errorsArray = this.getErrorsArray();
            if (errorsArray.length !== 0) {
                errorsHtml = "

";
                errorsHtml += errorsArray.join("

");
                errorsHtml += "

";
            }
            return errorsHtml;
        },
        /**
         * Shows a pop up formatted with HTML with all the errors of the `errors` object in it
         * @return void
         */
        showErrorsPopup: function () {
            if (!this.isValid()) {
                this.$.errorsPopup.setContent(this.getErrorsHtml());
                this.$.errorsPopup.show();
            }
        },
        validate: function (control) {
            control = control || this;
            if ( control === this ) {
                // this reffers to the present control which is the container of all inputs - form
                this.errors = {};
                validateForm.call(this, this);
            } else {
                // when specific control is given as an argument
                this.errors = this.errors || {};
                this.errors[control.name] = [];
                validateControl.call(this, control);
            }
        },
        _handleChange: function(inSender, inEvent) {
            var control = inEvent.originator;
            if( this.getLive() && shouldValidate(control) ){
                this.validate(control);
                if( keyHasError.call(this,control.name) ){
                    this.doLiveError(control);
                } else {
                    this.doLiveSuccess(control);
                }
            }
        }
    });
})(enyo);
<|endoftext|>


<commit_before>var last_known_path = null;

function playback_started(event) {
    last_known_path = mp.get_property('path');
}

function is_vifm_running() {
    return mp.utils.subprocess({
        args: ['pidof', 'vifm'],
        cancellable: false,
    })['status'] == 0;
}

function get_vifm_server_name() {
    return mp.utils.getenv('VIFM_SERVER_NAME');
}

// TODO: https://github.com/vifm/vifm/issues/293
function get_vifm_info(callback) {
    var vifm_server_name = get_vifm_server_name();
    var tmp_file = '/tmp/tmp.txt';

    args = ['vifm'];
    if (vifm_server_name) {
        args.push('--server-name');
        args.push(vifm_server_name);
    }
    args = args.concat([
        '--remote',
        '-c', 'execute "!echo " paneisat("left") ">' + tmp_file + '"',
        '-c', 'execute "!echo " expand("%c") ">>' + tmp_file + '"',
        '-c', 'execute "!echo " expand("%C") ">>' + tmp_file + '"',
        '-c', 'execute "!echo " expand("%d") ">>' + tmp_file + '"',
        '-c', 'execute "!echo " expand("%D") ">>' + tmp_file + '"',
    ]);
    var result = mp.utils.subprocess({args: args, cancellable: false});

    setTimeout(function() {
        var lines = mp.utils.read_file(tmp_file).split(/\n/);
        var info = {
            selected_pane: parseInt(lines[0]),
            current_file: lines[1],
            other_file: lines[2],
            current_dir: lines[3],
            other_dir: lines[4],
        };
        callback(info);
    }, 100);
}

function run_vifm() {
    if (!is_vifm_running()) {
        mp.utils.subprocess_detached({args: ['urxvt', '-e', 'vifm']});
        mp.command('quit');
    }

    var vifm_server_name = get_vifm_server_name();
    get_vifm_info(function(vifm_info) {
        print(vifm_info.current_file);
        print(vifm_info.other_file);

        var args = ['vifm'];
        if (vifm_server_name) {
            args.push('--server-name');
            args.push(vifm_server_name);
        }
        args.push('--remote');
        if (vifm_info.selected_pane === 1) {
            print('mode2');
            args.push('--select');
            args.push(last_known_path);
            // omitting the other argument leaves the other pane intact
        } else {
            print('mode1');
            // provide the first pane
            args.push('--select');
            if (vifm_info.other_file.match(/\.\.$/)) {
                args.push(vifm_info.other_file.replace(/\.\./, ''));
            } else {
                args.push(vifm_info.other_file);
            }
            args.push('--select');
            args.push(last_known_path);
        }
        mp.utils.subprocess_detached({args: args});
        mp.command('quit');
    });
}

mp.register_script_message('run-vifm', run_vifm);
mp.add_hook('on_load', 50, playback_started);
<commit_msg>cfg/mpi: clean up run-vifm; fix selection<commit_after>var last_known_path = null;

function playback_started(event) {
    last_known_path = mp.get_property('path');
}

function is_vifm_running() {
    return mp.utils.subprocess({
        args: ['pidof', 'vifm'],
        cancellable: false,
    })['status'] == 0;
}

function get_vifm_server_name() {
    return mp.utils.getenv('VIFM_SERVER_NAME');
}

// TODO: https://github.com/vifm/vifm/issues/293
function get_vifm_info(callback) {
    var vifm_server_name = get_vifm_server_name();
    var tmp_file = '/tmp/tmp.txt';

    args = ['vifm'];
    if (vifm_server_name) {
        args.push('--server-name');
        args.push(vifm_server_name);
    }
    args = args.concat([
        '--remote',
        '-c', 'execute "!echo " paneisat("left") ">' + tmp_file + '"',
        '-c', '!echo %c >>' + tmp_file,
        '-c', '!echo %C >>' + tmp_file,
        '-c', '!echo %d >>' + tmp_file,
        '-c', '!echo %D >>' + tmp_file,
    ]);
    var result = mp.utils.subprocess({args: args, cancellable: false});

    setTimeout(function() {
        var lines = mp.utils.read_file(tmp_file).split(/\n/);
        var info = {
            selected_pane: parseInt(lines[0]),
            current_file: lines[1],
            other_file: lines[2],
            current_dir: lines[3],
            other_dir: lines[4],
        };
        callback(info);
    }, 100);
}

function run_vifm() {
    if (!is_vifm_running()) {
        mp.utils.subprocess_detached({args: ['urxvt', '-e', 'vifm']});
        mp.command('quit');
    }

    var vifm_server_name = get_vifm_server_name();
    get_vifm_info(function(vifm_info) {
        var args = ['vifm'];
        if (vifm_server_name) {
            args.push('--server-name');
            args.push(vifm_server_name);
        }
        args.push('--remote');
        if (vifm_info.selected_pane === 1) {
            args.push('--select');
            args.push(last_known_path);
        } else {
            args.push(vifm_info.other_dir);
            args.push('--select');
            args.push(last_known_path);
        }
        mp.utils.subprocess_detached({args: args});
        mp.command('quit');
    });
}

mp.register_script_message('run-vifm', run_vifm);
mp.add_hook('on_load', 50, playback_started);
<|endoftext|>


<commit_before>ko.validation.rules.isUnique = {
  validator: function (newVal, options, foo) {
    if (options.predicate && typeof options.predicate !== "function")
      throw new Error("Invalid option for isUnique validator. The 'predicate' option must be a function.");

    var array = options.array || options;
    var count = 0;
    ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (existingVal) {
      if (equalityDelegate()(existingVal, newVal)) count++;
    });
    return count < 1;

    function equalityDelegate() {
      return options.predicate ? options.predicate : function (v1, v2) {
        return v1 === v2;
      };
    }
  },
  message: 'This value is a duplicate'
};
<commit_msg>Cleanup layout and make fadeIn/Out configurable<commit_after>ko.validation.rules.isUnique = {
  validator: function (newVal, options, foo) {
    if (options.predicate && typeof options.predicate !== "function")
      throw new Error("Invalid option for isUnique validator. The 'predicate' option must be a function.");

    var array = options.array || options;
    var count = 0;
    ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (existingVal) {
      if (equalityDelegate()(existingVal, newVal)) count++;
    });
    return count < 1;

    function equalityDelegate() {
      return options.predicate ? options.predicate : function (v1, v2) {
        return v1 === v2;
      };
    }
  },
  message: 'This value is a duplicate'
};

ko.bindingHandlers.fadeVisible = {
  init: function(element, valueAccessor) {
    // Initially set the element to be instantly visible/hidden depending on the value
    var value = valueAccessor();
    var itemValue = value;
    if (typeof value == "object") { // passing in parameters
      itemValue = value.item;
    }
    $(element).toggle(ko.utils.unwrapObservable(itemValue)); // Use "unwrapObservable" so we can handle values that may or may not be observable
  },
  update: function(element, valueAccessor) {
    // Whenever the value subsequently changes, slowly fade the element in or out
    var value = valueAccessor();
    var itemValue = value;
    var fadeIn = true;
    var fadeOut = true;
    if (typeof value == "object") { // passing in parameters
      itemValue = value.value;
      if ('fadeIn' in value) {
        fadeIn = value.fadeIn;
      }
      if ('fadeOut' in value) {
        fadeOut = value.fadeOut;
      }
    }
    if (ko.utils.unwrapObservable(itemValue)) {
      fadeIn ? $(element).fadeIn() : $(element).show();
    }
    else {
      fadeOut ? $(element).fadeOut() : $(element).hide();
    }
  }
};<|endoftext|>


<commit_before>import React from 'react';
import moment from 'moment';
import SunCalc from 'suncalc';
import styled from 'styled-components';

import Tile from './tile';

const VIEWBOX_SIZE = 100;
const CX = VIEWBOX_SIZE / 2;
const CY = VIEWBOX_SIZE / 2;
const RADIUS = VIEWBOX_SIZE / 2;
const CELESTIAL_BODY_SIZE = 16;

const ApplyClockBg = styled.div`
  .background {
    fill: ${p => p.theme.colors.white};
  }
  .time {
    fill: ${p => p.theme.colors.black};
    color: ${p => p.theme.colors.black};
  }
  .date {
    fill: ${p => p.theme.colors.gray};
  }
`;

const toRelativeTime = (date, referenceTime, unit) => moment(date)
  .diff(referenceTime, unit);

const minsToDegs = (mins) => {
  // 1440 = total minutes in an earth day
  return (mins / 1440) * 360;
};

const radToDeg = rad => rad * (180 / Math.PI);

const degToRad = deg => deg * (Math.PI / 180);

const convert = (date, referenceTime) => {
  return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'));
};

// https://github.com/tingletech/moon-phase
export const dFromPhase = (moonPhase) => {
  let mag, sweep, d = "m50,0";
  if (moonPhase <= 0.25) {
    sweep = [ 1, 0 ];
    mag = 20 - 20 * moonPhase * 4;
  } else if (moonPhase <= 0.50) {
    sweep = [ 0, 0 ];
    mag = 20 * (moonPhase - 0.25) * 4;
  } else if (moonPhase <= 0.75) {
    sweep = [ 1, 1 ];
    mag = 20 - 20 * (moonPhase - 0.50) * 4;
  } else if (moonPhase <= 1) {
    sweep = [ 0, 1 ];
    mag = 20 * (moonPhase - 0.75) * 4;
  }

  d = d + "a" + mag + ",20 0 1," + sweep[0] + " 0,100 ";
  d = d + "a20,20 0 1," + sweep[1] + " 0,-100";
  return d;
}

const Moon = ({ angle, ...props }) => {
  const phase = SunCalc.getMoonIllumination(moment().toDate()).phase.toFixed(2);
  const cx = CX + (RADIUS - 12) * Math.cos(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
  const cy = CY + (RADIUS - 12) * Math.sin(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
  return (
    <g>
      <mask id="umbra">
        <rect x="-50" y="-50" height="200" width="200" fill="black" />
        <path d={dFromPhase(phase)} fill="white"/>
      </mask>
      <use
      width={CELESTIAL_BODY_SIZE}
      height={CELESTIAL_BODY_SIZE}
      xlinkHref="#Moon-symbol"
      x={cx}
      y={cy}
      transform={`rotate(${angle} ${cx + (CELESTIAL_BODY_SIZE / 2)} ${cy + (CELESTIAL_BODY_SIZE / 2)})`}
      />
    </g>
  );
}

const Sun = ({ angle, ...props}) => (
  <circle
    id="sun"
    cx={CX + (RADIUS - 12) * Math.cos(degToRad(angle))}
    cy={CY + (RADIUS - 12) * Math.sin(degToRad(angle))}
    fill="#FCC440"
    stroke="rgba(0,0,0,0.1)"
    r={CELESTIAL_BODY_SIZE / 2}
    {...props}
  ></circle>
);

const SvgArc = ({ start, end, ...rest }) => {
  const x1 = CX + RADIUS * Math.cos(degToRad(start));
  const y1 = CY + RADIUS * Math.sin(degToRad(start));
  const x2 = CX + RADIUS * Math.cos(degToRad(end));
  const y2 = CY + RADIUS * Math.sin(degToRad(end));

  const isLarge = Math.abs((start > 360 ? start - 360 : start) - end) > 180;

  const d = [
    'M', CX, CY,
    'L', x1, y1,
    'A', RADIUS, RADIUS, '0', '1', '1', x2, y2, 'z'
  ].join(' ');

  return <path d={d} {...rest} />;
}

class ClockText extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: Date.now()
    }
  }
  componentDidMount() {
    this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const now = moment(this.state.time);
    return (
      <>
        <use xlinkHref="#clock-center" className="background" />
        <text
          textAnchor="middle"
          x={CX}
          y={CY - 2}
          fontSize="10"
          fontFamily="Inter"
          className="time"
        >
          <tspan>{now.format('h')}</tspan>
          <tspan>:<animate attributeName="fill"
            values="currentColor;transparent"
            begin="0s"
            dur="1s"
            calcMode="discrete"
            repeatCount="indefinite"/>
          </tspan>
          <tspan>{now.format('mm A')}</tspan>
        </text>
        <text
          textAnchor="middle"
          x={CX}
          y={CY + 11}
          fontSize="10"
          fontFamily="Inter"
          className="date"
      >{now.format('MMM D')}<tspan style={{ fontFeatureSettings: "'sups' 1" }}>{now.format('Do').replace(now.format('D'), '')}</tspan></text>
      </>
    );
  }
}

class Clock extends React.PureComponent {
  constructor(props) {
    super(props);
    this.angle = 0;
    this.referenceTime = moment().startOf('day').subtract(6, 'hours');
    this.state = {
      time: Date.now(),
      lat: 0,
      lon: 0,
      geolocationSuccess: false,
      sunrise: 0,
      sunsetStart: 0,
      sunset:0,
      sunriseEnd: 0,
      dusk: 0,
      dawn: 0,
      night: 0,
      nightEnd: 0,
      nauticalDawn: 0,
      nauticalDusk: 0
      // sunriseStartTime = 1509967519,
      // sunriseEndTime = 2500 + 1509967519,
      // sunsetStartTime = 1510003982,
      // sunsetEndTime
      // moonPhase = 0.59,
    };
  }

  initGeolocation() {
    if (typeof this.props.data === 'string') {
      const latlon = this.props.data.split(',');
      const lat = latlon[0];
      const lon = latlon[1];

      const suncalc = SunCalc.getTimes(new Date(), lat, lon);

      const convertedSunCalc = {
        sunset: convert(suncalc.sunset, this.referenceTime),
        sunrise: convert(suncalc.sunrise, this.referenceTime),
        sunsetStart: convert(suncalc.sunsetStart, this.referenceTime),
        sunriseEnd: convert(suncalc.sunriseEnd, this.referenceTime),
        dusk: convert(suncalc.dusk, this.referenceTime),
        dawn: convert(suncalc.dawn, this.referenceTime),
        night: convert(suncalc.night, this.referenceTime),
        nightEnd: convert(suncalc.nightEnd, this.referenceTime),
        nauticalDawn: convert(suncalc.nauticalDawn, this.referenceTime),
        nauticalDusk: convert(suncalc.nauticalDusk, this.referenceTime)
      };

      this.setState({
        lat,
        lon,
        ...convertedSunCalc,
        geolocationSuccess: true
      });
    }
  }

  componentDidUpdate(prevProps) {
    if (prevProps !== this.props) {
      this.initGeolocation();
    }
  }

  componentDidMount() {
    this.initGeolocation();
    this.interval = setInterval(() => this.setState({ time: Date.now() }), 60000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const now = moment(this.state.time);
    const angle = convert(now, this.referenceTime);

    return (
      <ApplyClockBg>
        <svg
          style={{ height: '100%', width: '100%'}}
          viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
          xmlns="http://www.w3.org/2000/svg"
          xmlnsXlink="http://www.w3.org/1999/xlink"
        >
          <defs>
            <symbol id="border">
              <circle cx={CY} cy={CY} r={RADIUS} />
            </symbol>
            <symbol id="clock-center">
              <circle r={VIEWBOX_SIZE / 1.85 / 2} cx={CX} cy={CY} />
            </symbol>
            <mask id="center-mask">
              <use xlinkHref="#border" fill="white" />
              <use xlinkHref="#clock-center" fill="black"/>
            </mask>
            <symbol id="Moon-symbol" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
              <g>
                <path mask="url(#umbra)" d="m50,0 a20,20 0 1,1 0,100 a20,20 0 1,1 0,-100" fill="#fff" stroke="#000"/>
              </g>
            </symbol>

          </defs>
          <g mask="url(#center-mask)">
            <use xlinkHref="#border" className="background" />
            <SvgArc
              id="day"
              start={this.state.sunriseEnd}
              end={this.state.sunset}
              fill="rgba(33, 157, 255, .2)"
            />
            <SvgArc
              id="sunrise"
              start={this.state.sunsetStart}
              end={this.state.sunriseEnd}
              fill="#FFC700"
            />
            <SvgArc
              id="sunset"
              start={this.state.dusk}
              end={this.state.dawn}
              fill="rgba(255, 65, 54, .8)"
            />
            <SvgArc
              id="night"
              start={this.state.night}
              end={this.state.nightEnd}
              fill="rgba(0, 0, 0, .8)"
            />
            {angle > this.state.nightEnd && angle < this.state.sunset
              ? <Sun angle={angle} />
              : <Moon angle={angle} />
            }
          </g>
          <ClockText />
        </svg>
      </ApplyClockBg>
    );
  }
}

const ClockTile = ({ location = {} }) => (
  <Tile p={0} border={0} bg='transparent' boxShadow='none'>
    <Clock data={location} />
  </Tile>
);

export default ClockTile;<commit_msg>clock: add background to moon<commit_after>import React from 'react';
import moment from 'moment';
import SunCalc from 'suncalc';
import styled from 'styled-components';

import Tile from './tile';

const VIEWBOX_SIZE = 100;
const CX = VIEWBOX_SIZE / 2;
const CY = VIEWBOX_SIZE / 2;
const RADIUS = VIEWBOX_SIZE / 2;
const CELESTIAL_BODY_SIZE = 16;

const ApplyClockBg = styled.div`
  .background {
    fill: ${p => p.theme.colors.white};
  }
  .time {
    fill: ${p => p.theme.colors.black};
    color: ${p => p.theme.colors.black};
  }
  .date {
    fill: ${p => p.theme.colors.gray};
  }
`;

const toRelativeTime = (date, referenceTime, unit) => moment(date)
  .diff(referenceTime, unit);

const minsToDegs = (mins) => {
  // 1440 = total minutes in an earth day
  return (mins / 1440) * 360;
};

const radToDeg = rad => rad * (180 / Math.PI);

const degToRad = deg => deg * (Math.PI / 180);

const convert = (date, referenceTime) => {
  return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'));
};

// https://github.com/tingletech/moon-phase
export const dFromPhase = (moonPhase) => {
  let mag, sweep, d = "m50,0";
  if (moonPhase <= 0.25) {
    sweep = [ 1, 0 ];
    mag = 20 - 20 * moonPhase * 4;
  } else if (moonPhase <= 0.50) {
    sweep = [ 0, 0 ];
    mag = 20 * (moonPhase - 0.25) * 4;
  } else if (moonPhase <= 0.75) {
    sweep = [ 1, 1 ];
    mag = 20 - 20 * (moonPhase - 0.50) * 4;
  } else if (moonPhase <= 1) {
    sweep = [ 0, 1 ];
    mag = 20 * (moonPhase - 0.75) * 4;
  }

  d = d + "a" + mag + ",20 0 1," + sweep[0] + " 0,100 ";
  d = d + "a20,20 0 1," + sweep[1] + " 0,-100";
  return d;
}

const Moon = ({ angle, ...props }) => {
  const phase = SunCalc.getMoonIllumination(moment().toDate()).phase.toFixed(2);
  const cx = CX + (RADIUS - 12) * Math.cos(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
  const cy = CY + (RADIUS - 12) * Math.sin(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
  return (
    <g>
      <mask id="umbra">
        <rect x="-50" y="-50" height="200" width="200" fill="black" />
        <path d={dFromPhase(phase)} fill="white"/>
      </mask>
      <circle
        id="moonbg"
        cx={CX + (RADIUS - 12) * Math.cos(degToRad(angle))}
        cy={CY + (RADIUS - 12) * Math.sin(degToRad(angle))}
        fill="rgba(255,255,255,0.6)"
        r={CELESTIAL_BODY_SIZE / 2}
        {...props}
      ></circle>
      <use
      width={CELESTIAL_BODY_SIZE}
      height={CELESTIAL_BODY_SIZE}
      xlinkHref="#Moon-symbol"
      id="moon"
      x={cx}
      y={cy}
      transform={`rotate(${angle} ${cx + (CELESTIAL_BODY_SIZE / 2)} ${cy + (CELESTIAL_BODY_SIZE / 2)})`}
      />
    </g>
  );
}

const Sun = ({ angle, ...props}) => (
  <circle
    id="sun"
    cx={CX + (RADIUS - 12) * Math.cos(degToRad(angle))}
    cy={CY + (RADIUS - 12) * Math.sin(degToRad(angle))}
    fill="#FCC440"
    stroke="rgba(0,0,0,0.1)"
    r={CELESTIAL_BODY_SIZE / 2}
    {...props}
  ></circle>
);

const SvgArc = ({ start, end, ...rest }) => {
  const x1 = CX + RADIUS * Math.cos(degToRad(start));
  const y1 = CY + RADIUS * Math.sin(degToRad(start));
  const x2 = CX + RADIUS * Math.cos(degToRad(end));
  const y2 = CY + RADIUS * Math.sin(degToRad(end));

  const isLarge = Math.abs((start > 360 ? start - 360 : start) - end) > 180;

  const d = [
    'M', CX, CY,
    'L', x1, y1,
    'A', RADIUS, RADIUS, '0', '1', '1', x2, y2, 'z'
  ].join(' ');

  return <path d={d} {...rest} />;
}

class ClockText extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: Date.now()
    }
  }
  componentDidMount() {
    this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const now = moment(this.state.time);
    return (
      <>
        <use xlinkHref="#clock-center" className="background" />
        <text
          textAnchor="middle"
          x={CX}
          y={CY - 2}
          fontSize="10"
          fontFamily="Inter"
          className="time"
        >
          <tspan>{now.format('h')}</tspan>
          <tspan>:<animate attributeName="fill"
            values="currentColor;transparent"
            begin="0s"
            dur="1s"
            calcMode="discrete"
            repeatCount="indefinite"/>
          </tspan>
          <tspan>{now.format('mm A')}</tspan>
        </text>
        <text
          textAnchor="middle"
          x={CX}
          y={CY + 11}
          fontSize="10"
          fontFamily="Inter"
          className="date"
      >{now.format('MMM D')}<tspan style={{ fontFeatureSettings: "'sups' 1" }}>{now.format('Do').replace(now.format('D'), '')}</tspan></text>
      </>
    );
  }
}

class Clock extends React.PureComponent {
  constructor(props) {
    super(props);
    this.angle = 0;
    this.referenceTime = moment().startOf('day').subtract(6, 'hours');
    this.state = {
      time: Date.now(),
      lat: 0,
      lon: 0,
      geolocationSuccess: false,
      sunrise: 0,
      sunsetStart: 0,
      sunset:0,
      sunriseEnd: 0,
      dusk: 0,
      dawn: 0,
      night: 0,
      nightEnd: 0,
      nauticalDawn: 0,
      nauticalDusk: 0
      // sunriseStartTime = 1509967519,
      // sunriseEndTime = 2500 + 1509967519,
      // sunsetStartTime = 1510003982,
      // sunsetEndTime
      // moonPhase = 0.59,
    };
  }

  initGeolocation() {
    if (typeof this.props.data === 'string') {
      const latlon = this.props.data.split(',');
      const lat = latlon[0];
      const lon = latlon[1];

      const suncalc = SunCalc.getTimes(new Date(), lat, lon);

      const convertedSunCalc = {
        sunset: convert(suncalc.sunset, this.referenceTime),
        sunrise: convert(suncalc.sunrise, this.referenceTime),
        sunsetStart: convert(suncalc.sunsetStart, this.referenceTime),
        sunriseEnd: convert(suncalc.sunriseEnd, this.referenceTime),
        dusk: convert(suncalc.dusk, this.referenceTime),
        dawn: convert(suncalc.dawn, this.referenceTime),
        night: convert(suncalc.night, this.referenceTime),
        nightEnd: convert(suncalc.nightEnd, this.referenceTime),
        nauticalDawn: convert(suncalc.nauticalDawn, this.referenceTime),
        nauticalDusk: convert(suncalc.nauticalDusk, this.referenceTime)
      };

      this.setState({
        lat,
        lon,
        ...convertedSunCalc,
        geolocationSuccess: true
      });
    }
  }

  componentDidUpdate(prevProps) {
    if (prevProps !== this.props) {
      this.initGeolocation();
    }
  }

  componentDidMount() {
    this.initGeolocation();
    this.interval = setInterval(() => this.setState({ time: Date.now() }), 60000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const now = moment(this.state.time);
    const angle = convert(now, this.referenceTime);

    return (
      <ApplyClockBg>
        <svg
          style={{ height: '100%', width: '100%'}}
          viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
          xmlns="http://www.w3.org/2000/svg"
          xmlnsXlink="http://www.w3.org/1999/xlink"
        >
          <defs>
            <symbol id="border">
              <circle cx={CY} cy={CY} r={RADIUS} />
            </symbol>
            <symbol id="clock-center">
              <circle r={VIEWBOX_SIZE / 1.85 / 2} cx={CX} cy={CY} />
            </symbol>
            <mask id="center-mask">
              <use xlinkHref="#border" fill="white" />
              <use xlinkHref="#clock-center" fill="black"/>
            </mask>
            <symbol id="Moon-symbol" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
              <g>
                <path mask="url(#umbra)" d="m50,0 a20,20 0 1,1 0,100 a20,20 0 1,1 0,-100" fill="#fff" stroke="#000"/>
              </g>
            </symbol>

          </defs>
          <g mask="url(#center-mask)">
            <use xlinkHref="#border" className="background" />
            <SvgArc
              id="day"
              start={this.state.sunriseEnd}
              end={this.state.sunset}
              fill="rgba(33, 157, 255, .2)"
            />
            <SvgArc
              id="sunrise"
              start={this.state.sunsetStart}
              end={this.state.sunriseEnd}
              fill="#FFC700"
            />
            <SvgArc
              id="sunset"
              start={this.state.dusk}
              end={this.state.dawn}
              fill="rgba(255, 65, 54, .8)"
            />
            <SvgArc
              id="night"
              start={this.state.night}
              end={this.state.nightEnd}
              fill="rgba(0, 0, 0, .8)"
            />
            {angle > this.state.nightEnd && angle < this.state.sunset
              ? <Sun angle={angle} />
              : <Moon angle={angle} />
            }
          </g>
          <ClockText />
        </svg>
      </ApplyClockBg>
    );
  }
}

const ClockTile = ({ location = {} }) => (
  <Tile p={0} border={0} bg='transparent' boxShadow='none'>
    <Clock data={location} />
  </Tile>
);

export default ClockTile;<|endoftext|>


<commit_before>var saveData = require("splat-ecs/lib/save-data");
var moment = require("moment");

var levels = require("../../data/levels.json");

var dateFormatCache = {};

var blinkPlayerTime = false;
setInterval(function togglePlayerTimeBlink(){
  blinkPlayerTime = !blinkPlayerTime;
}, 500);

function drawText(text, fontSize, y, lineWidth, canvas, context) {
  context.fillStyle = "#3e311a";
  context.strokeStyle = "#ffffff";
  context.lineWidth = lineWidth;
  context.font = fontSize + "px blanch";
  var w = context.measureText(text).width;
  var x = (canvas.width / 2) - (w / 2) | 0;
  context.strokeText(text, x, y);
  context.fillText(text, x, y);
}

function isoStringToFormatted(isoString) {
  if (dateFormatCache[isoString]) {
    return dateFormatCache[isoString];
  }

  var formatted = moment(isoString).format("dddd h:mma");
  dateFormatCache[isoString] = formatted;
  return formatted;
}

module.exports = function(ecs, game) { // eslint-disable-line no-unused-vars
  var renderedBestTimes = null;

  function getRenderedBestTimes(entity, callback) {
    var levelIndex = game.arguments.level || 0;
    var storageKey = "scores-" + levelIndex;
    saveData.get(storageKey, function renderBestTimes(err, data) {
      if (err || !data[storageKey]) {
        return callback(entity);
      }
      var bestTimes = JSON.parse(data[storageKey]);
      if (!bestTimes || !bestTimes.length) {
        return callback(entity);
      }
      renderedBestTimes = [];
      var longestRowLength = 0;
      // turn each record into a formatted string
      var i;
      for (i = 0; i < bestTimes.length; i++) {
        var record = bestTimes[i];
        renderedBestTimes[i] = (
          (record.time / 1000).toFixed(1) + "s" +
          "     " +
          isoStringToFormatted(record.when) +
          "\n"
        );
        longestRowLength = Math.max(longestRowLength, renderedBestTimes[i].length);
      }
      // do another pass to make sure each row is the same length
      for (i = 0; i < renderedBestTimes.length; i++) {
        while (renderedBestTimes[i].length < longestRowLength) {
          var spaceIndex = renderedBestTimes[i].indexOf(" ");
          renderedBestTimes[i] = (
            renderedBestTimes[i].slice(0, spaceIndex) +
            " " +
            renderedBestTimes[i].slice(spaceIndex)
          );
        }
      }
      callback(entity);
    });
  }

  function renderResults(entity) {
    var levelIndex = game.arguments.level || 0;
    var level = levels[levelIndex];

    var playerRadius = game.entities.getComponent(entity, "radius");
    var goalRadius = level.goalRadius;
    var statusMessage = playerRadius >= goalRadius ? "You're really big!" : "Not this time...";
    statusMessage += " (" + Math.floor(playerRadius) + "m)";
    var successTime = game.entities.getComponent(entity, "successTime");
    var successTimeMessage = successTime && "Goal reached at " + (successTime / 1000).toFixed(1) + "s";

    var canvas = game.canvas;
    var context = game.context;
    var camera = game.entities.find("camera")[0];
    var cameraHeight = game.entities.getComponent(camera, "size").height;

    var drawY = 200;

    drawText(level.message, 100, drawY, 10, canvas, context);
    drawY += 100 /* * canvas.height / cameraHeight*/;

    drawText(statusMessage, 60, drawY, 6, canvas, context);
    drawY += 60 /* * canvas.height / cameraHeight*/;

    if (successTimeMessage) {
      drawText(successTimeMessage, 60, drawY, 6, canvas, context);
      drawY += 60 /* * canvas.height / cameraHeight*/;
    }

    drawY += 15 /* * canvas.height / cameraHeight*/;

    drawText("Best Times", 60, drawY, 6, canvas, context);
    drawY += 60 /* * canvas.height / cameraHeight*/;

    var playerTimeFound = false;
    for (var i = 0; renderedBestTimes && drawY <= canvas.height && i < renderedBestTimes.length; i++) {
      var record = renderedBestTimes[i];
      if (
        successTime &&
        blinkPlayerTime &&
        !playerTimeFound &&
        record.indexOf((successTime / 1000).toFixed(1)) === 0
      ) {
        playerTimeFound = true;
      } else {
        drawText(renderedBestTimes[i], 50, drawY, 5, canvas, context);
      }
      drawY += 50 /* * canvas.height / cameraHeight*/;
    }
  }

  ecs.addEach(function showResults(entity, elapsed) { // eslint-disable-line no-unused-vars
    if (!renderedBestTimes || !renderedBestTimes.length) {
      return getRenderedBestTimes(entity, renderResults);
    }
    renderResults(entity);
  }, "showResults");
};
<commit_msg>Results display improved.<commit_after>"use strict";

var saveData = require("splat-ecs/lib/save-data");
var moment = require("moment");

var levels = require("../../data/levels.json");

var dateFormatCache = {};

var blinkPlayerTime = false;
setInterval(function togglePlayerTimeBlink(){
  blinkPlayerTime = !blinkPlayerTime;
}, 500);

function drawText(text, fontSize, y, lineWidth, canvas, context) {
  context.fillStyle = "#3e311a";
  context.strokeStyle = "#ffffff";
  context.lineWidth = lineWidth;
  context.font = fontSize + "px blanch";
  var w = context.measureText(text).width;
  var x = (canvas.width / 2) - (w / 2) | 0;
  context.strokeText(text, x, y);
  context.fillText(text, x, y);
}

function isoStringToFormatted(isoString) {
  if (dateFormatCache[isoString]) {
    return dateFormatCache[isoString];
  }

  var formatted = moment(isoString).format("dddd h:mma");
  dateFormatCache[isoString] = formatted;
  return formatted;
}

module.exports = function(ecs, game) { // eslint-disable-line no-unused-vars
  var renderedBestTimes = null;

  function getRenderedBestTimes(entity, callback) {
    var levelIndex = game.arguments.level || 0;
    var storageKey = "scores-" + levelIndex;
    saveData.get(storageKey, function renderBestTimes(err, data) {
      if (err || !data[storageKey]) {
        return callback(entity);
      }
      var bestTimes = JSON.parse(data[storageKey]);
      if (!bestTimes || !bestTimes.length) {
        return callback(entity);
      }
      renderedBestTimes = [];
      var longestRowLength = 0;
      // turn each record into a formatted string
      var i;
      for (i = 0; i < bestTimes.length; i++) {
        var record = bestTimes[i];
        renderedBestTimes[i] = (
          (record.time / 1000).toFixed(1) + "s" +
          "    " +
          isoStringToFormatted(record.when) +
          "\n"
        );
        longestRowLength = Math.max(longestRowLength, renderedBestTimes[i].length);
      }
      // do another pass to make sure each row is the same length
      for (i = 0; i < renderedBestTimes.length; i++) {
        while (renderedBestTimes[i].length < longestRowLength) {
          var spaceIndex = renderedBestTimes[i].indexOf(" ");
          renderedBestTimes[i] = (
            renderedBestTimes[i].slice(0, spaceIndex) +
            " " +
            renderedBestTimes[i].slice(spaceIndex)
          );
        }
      }
      callback(entity);
    });
  }

  function renderResults(entity) {
    var levelIndex = game.arguments.level || 0;
    var level = levels[levelIndex];

    var playerRadius = game.entities.getComponent(entity, "radius");
    var goalRadius = level.goalRadius;
    var statusMessage = playerRadius >= goalRadius ? "You're really big!" : "Not this time...";
    statusMessage += " (" + Math.floor(playerRadius) + "m)";
    var successTime = game.entities.getComponent(entity, "successTime");
    var successTimeMessage = typeof successTime === 'number' && "Goal reached in " + (successTime / 1000).toFixed(1) + "s";

    var canvas = game.canvas;
    var context = game.context;
    var camera = game.entities.find("camera")[0];
    var cameraHeight = game.entities.getComponent(camera, "size").height;

    var drawY = 150;

    drawText(level.message, 70, drawY, 10, canvas, context);
    drawY += 55 /* * canvas.height / cameraHeight*/;

    drawText(statusMessage, 50, drawY, 6, canvas, context);
    drawY += 40 /* * canvas.height / cameraHeight*/;

    if (successTimeMessage) {
      drawText(successTimeMessage, 50, drawY, 6, canvas, context);
      drawY += 40 /* * canvas.height / cameraHeight*/;
    }

    drawY += 10 /* * canvas.height / cameraHeight*/;

    drawText("Best Times", 45, drawY, 6, canvas, context);
    drawY += 45 /* * canvas.height / cameraHeight*/;

    var playerTimeFound = false;
    for (var i = 0; renderedBestTimes && drawY <= canvas.height && i < renderedBestTimes.length; i++) {
      var record = renderedBestTimes[i];
      if (
        blinkPlayerTime &&
        !playerTimeFound &&
        typeof successTime === "number" &&
        record.indexOf((successTime / 1000).toFixed(1)) === 0
      ) {
        playerTimeFound = true;
      } else {
        drawText(i + 1 + ".   " + renderedBestTimes[i], 38, drawY, 5, canvas, context);
      }
      drawY += 34 /* * canvas.height / cameraHeight*/;
    }
  }

  ecs.addEach(function showResults(entity, elapsed) { // eslint-disable-line no-unused-vars
    if (!renderedBestTimes || !renderedBestTimes.length) {
      return getRenderedBestTimes(entity, renderResults);
    }
    renderResults(entity);
  }, "showResults");
};
<|endoftext|>


<commit_before>function noop() {}

function assign(target) {
	var k,
		source,
		i = 1,
		len = arguments.length;
	for (; i < len; i++) {
		source = arguments[i];
		for (k in source) target[k] = source[k];
	}

	return target;
}

function blankObject() {
	return Object.create(null);
}

function destroy(detach) {
	this.destroy = noop;
	this.fire('destroy');
	this.set = this.get = noop;

	if (detach !== false) this._fragment.unmount();
	this._fragment.destroy();
	this._fragment = this._state = null;
}

function differs(a, b) {
	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}

function dispatchObservers(component, group, changed, newState, oldState) {
	for (var key in group) {
		if (!changed[key]) continue;

		var newValue = newState[key];
		var oldValue = oldState[key];

		var callbacks = group[key];
		if (!callbacks) continue;

		for (var i = 0; i < callbacks.length; i += 1) {
			var callback = callbacks[i];
			if (callback.__calling) continue;

			callback.__calling = true;
			callback.call(component, newValue, oldValue);
			callback.__calling = false;
		}
	}
}

function fire(eventName, data) {
	var handlers =
		eventName in this._handlers && this._handlers[eventName].slice();
	if (!handlers) return;

	for (var i = 0; i < handlers.length; i += 1) {
		handlers[i].call(this, data);
	}
}

function get(key) {
	return key ? this._state[key] : this._state;
}

function init(component, options) {
	component.options = options;

	component._observers = { pre: blankObject(), post: blankObject() };
	component._handlers = blankObject();
	component._root = options._root || component;
	component._yield = options._yield;
	component._bind = options._bind;
}

function observe(key, callback, options) {
	var group = options && options.defer
		? this._observers.post
		: this._observers.pre;

	(group[key] || (group[key] = [])).push(callback);

	if (!options || options.init !== false) {
		callback.__calling = true;
		callback.call(this, this._state[key]);
		callback.__calling = false;
	}

	return {
		cancel: function() {
			var index = group[key].indexOf(callback);
			if (~index) group[key].splice(index, 1);
		}
	};
}

function on(eventName, handler) {
	if (eventName === 'teardown') return this.on('destroy', handler);

	var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
	handlers.push(handler);

	return {
		cancel: function() {
			var index = handlers.indexOf(handler);
			if (~index) handlers.splice(index, 1);
		}
	};
}

function set(newState) {
	this._set(assign({}, newState));
	if (this._root._lock) return;
	this._root._lock = true;
	callAll(this._root._beforecreate);
	callAll(this._root._oncreate);
	callAll(this._root._aftercreate);
	this._root._lock = false;
}

function _set(newState) {
	var oldState = this._state,
		changed = {},
		dirty = false;

	for (var key in newState) {
		if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
	}
	if (!dirty) return;

	this._state = assign({}, oldState, newState);
	this._recompute(changed, this._state, oldState, false);
	if (this._bind) this._bind(changed, this._state);
	dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
	this._fragment.update(changed, this._state);
	dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}

function callAll(fns) {
	while (fns && fns.length) fns.pop()();
}

function _mount(target, anchor) {
	this._fragment.mount(target, anchor);
}

function _unmount() {
	this._fragment.unmount();
}

var proto = {
	destroy: destroy,
	get: get,
	fire: fire,
	observe: observe,
	on: on,
	set: set,
	teardown: destroy,
	_recompute: noop,
	_set: _set,
	_mount: _mount,
	_unmount: _unmount
};

/* generated by Svelte vX.Y.Z */

var template = (function() {
	return {
		components: {
			Nested: window.Nested
		}
	};
}());

function create_main_fragment(state, component) {

	var nested = new template.components.Nested({
		_root: component._root,
		data: { foo: "bar" }
	});

	return {
		create: function() {
			nested._fragment.create();
		},

		mount: function(target, anchor) {
			nested._mount(target, anchor);
		},

		update: noop,

		unmount: function() {
			nested._unmount();
		},

		destroy: function() {
			nested.destroy(false);
		}
	};
}

function SvelteComponent(options) {
	init(this, options);
	this._state = options.data || {};

	if (!options._root) {
		this._oncreate = [];
		this._beforecreate = [];
		this._aftercreate = [];
	}

	this._fragment = create_main_fragment(this._state, this);

	if (options.target) {
		this._fragment.create();
		this._fragment.mount(options.target, options.anchor || null);

		this._lock = true;
		callAll(this._beforecreate);
		callAll(this._oncreate);
		callAll(this._aftercreate);
		this._lock = false;
	}
}

assign(SvelteComponent.prototype, proto);

export default SvelteComponent;
<commit_msg>update tests<commit_after>function noop() {}

function assign(target) {
	var k,
		source,
		i = 1,
		len = arguments.length;
	for (; i < len; i++) {
		source = arguments[i];
		for (k in source) target[k] = source[k];
	}

	return target;
}

function blankObject() {
	return Object.create(null);
}

function destroy(detach) {
	this.destroy = noop;
	this.fire('destroy');
	this.set = this.get = noop;

	if (detach !== false) this._fragment.unmount();
	this._fragment.destroy();
	this._fragment = this._state = null;
}

function differs(a, b) {
	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}

function dispatchObservers(component, group, changed, newState, oldState) {
	for (var key in group) {
		if (!changed[key]) continue;

		var newValue = newState[key];
		var oldValue = oldState[key];

		var callbacks = group[key];
		if (!callbacks) continue;

		for (var i = 0; i < callbacks.length; i += 1) {
			var callback = callbacks[i];
			if (callback.__calling) continue;

			callback.__calling = true;
			callback.call(component, newValue, oldValue);
			callback.__calling = false;
		}
	}
}

function fire(eventName, data) {
	var handlers =
		eventName in this._handlers && this._handlers[eventName].slice();
	if (!handlers) return;

	for (var i = 0; i < handlers.length; i += 1) {
		handlers[i].call(this, data);
	}
}

function get(key) {
	return key ? this._state[key] : this._state;
}

function init(component, options) {
	component.options = options;

	component._observers = { pre: blankObject(), post: blankObject() };
	component._handlers = blankObject();
	component._root = options._root || component;
	component._yield = options._yield;
	component._bind = options._bind;
}

function observe(key, callback, options) {
	var group = options && options.defer
		? this._observers.post
		: this._observers.pre;

	(group[key] || (group[key] = [])).push(callback);

	if (!options || options.init !== false) {
		callback.__calling = true;
		callback.call(this, this._state[key]);
		callback.__calling = false;
	}

	return {
		cancel: function() {
			var index = group[key].indexOf(callback);
			if (~index) group[key].splice(index, 1);
		}
	};
}

function on(eventName, handler) {
	if (eventName === 'teardown') return this.on('destroy', handler);

	var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
	handlers.push(handler);

	return {
		cancel: function() {
			var index = handlers.indexOf(handler);
			if (~index) handlers.splice(index, 1);
		}
	};
}

function set(newState) {
	this._set(assign({}, newState));
	if (this._root._lock) return;
	this._root._lock = true;
	callAll(this._root._beforecreate);
	callAll(this._root._oncreate);
	callAll(this._root._aftercreate);
	this._root._lock = false;
}

function _set(newState) {
	var oldState = this._state,
		changed = {},
		dirty = false;

	for (var key in newState) {
		if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
	}
	if (!dirty) return;

	this._state = assign({}, oldState, newState);
	this._recompute(changed, this._state);
	if (this._bind) this._bind(changed, this._state);
	dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
	this._fragment.update(changed, this._state);
	dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}

function callAll(fns) {
	while (fns && fns.length) fns.pop()();
}

function _mount(target, anchor) {
	this._fragment.mount(target, anchor);
}

function _unmount() {
	this._fragment.unmount();
}

var proto = {
	destroy: destroy,
	get: get,
	fire: fire,
	observe: observe,
	on: on,
	set: set,
	teardown: destroy,
	_recompute: noop,
	_set: _set,
	_mount: _mount,
	_unmount: _unmount
};

/* generated by Svelte vX.Y.Z */

var template = (function() {
	return {
		components: {
			Nested: window.Nested
		}
	};
}());

function create_main_fragment(state, component) {

	var nested = new template.components.Nested({
		_root: component._root,
		data: { foo: "bar" }
	});

	return {
		create: function() {
			nested._fragment.create();
		},

		mount: function(target, anchor) {
			nested._mount(target, anchor);
		},

		update: noop,

		unmount: function() {
			nested._unmount();
		},

		destroy: function() {
			nested.destroy(false);
		}
	};
}

function SvelteComponent(options) {
	init(this, options);
	this._state = options.data || {};

	if (!options._root) {
		this._oncreate = [];
		this._beforecreate = [];
		this._aftercreate = [];
	}

	this._fragment = create_main_fragment(this._state, this);

	if (options.target) {
		this._fragment.create();
		this._fragment.mount(options.target, options.anchor || null);

		this._lock = true;
		callAll(this._beforecreate);
		callAll(this._oncreate);
		callAll(this._aftercreate);
		this._lock = false;
	}
}

assign(SvelteComponent.prototype, proto);

export default SvelteComponent;
<|endoftext|>


<commit_before>/**
 * Plugin for setting a lower opacity for other series than the one that is hovered over.
 * Additionally, if not hovering over, a lower opacity is set based on series selected status.
 */
(function (Highcharts) {
    function highlightOn(allSeries, currentSeries) {
        return function (e) {
            $.each(allSeries, function (i, series) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                var current = (series === currentSeries) || (series.linkedParent === currentSeries) || (series.options.streamId === currentSeries.options.streamId);
                $.each(['group', 'markerGroup'], function (j, group) {
                    series[group].attr('opacity', current ? 1.0 : 0.25);
                });
                series.chart.legend.colorizeItem(series, current);
            });
        };
    }

    function highlightOff(allSeries, currentSeries) {
        return function (e) {
            $.each(allSeries, function (i, series) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                assert(series.options.streamId);

                var selected = series.selected || _.some(_.filter(allSeries, function (s) {return s.options.streamId === series.options.streamId}), function (s) {return s.selected});

                $.each(['group', 'markerGroup'], function (j, group) {
                    series[group].attr('opacity', selected ? 1.0 : 0.25);
                });
                series.chart.legend.colorizeItem(series, selected);
            });
        };
    }

    // Hovering over the legend
    Highcharts.wrap(Highcharts.Legend.prototype, 'renderItem', function (proceed, currentSeries) {
        proceed.call(this, currentSeries);

        var allSeries = this.chart.series;

        $(currentSeries.legendGroup.element).off('.highlight').on(
            'mouseenter.highlight', highlightOn(allSeries, currentSeries)
        ).on(
            'mouseleave.highlight', highlightOff(allSeries, currentSeries)
        );
    });
}(Highcharts));

// From: http://jsfiddle.net/unLSJ/

function prettyPrint(obj) {
    var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
    return JSON.stringify(obj, null, 3)
        .replace(/&/g, '&').replace(/\\"/g, '"')
        .replace(/</g, '<').replace(/>/g, '>')
        .replace(jsonLine, replacer);
}

function replacer(match, pIndent, pKey, pVal, pEnd) {
    var key = '';
    var val = '';
    var str = '';
    var r = pIndent || '';
    if (pKey)
        r = r + key + pKey.replace(/[": ]/g, '') + ': ';
    if (pVal)
        r = r + (pVal[0] == '"' ? str : val) + pVal + '';
    return r + (pEnd || '');
}

// TODO: This curently does not depend on how many datapoints are really available, so if granularity is seconds, it assumes that every second will have a datapoint
// TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
var MAX_POINTS_NUMBER = 300;

var granularities = [
    {'name': 'days', 'duration': 86400},
    {'name': '6hours', 'duration': 21600},
    {'name': 'hours', 'duration': 3600},
    {'name': '10minutes', 'duration': 600},
    {'name': 'minutes', 'duration': 60},
    {'name': '10seconds', 'duration': 10},
    {'name': 'seconds', 'duration': 1}
];

function firstDefined(obj) {
    for (var i = 1; i < arguments.length; i++) {
        if (typeof obj[arguments[i]] !== 'undefined') {
            return obj[arguments[i]];
        }
    }
}

function updateKnownMaxRange(stream) {
    assert(stream.id in activeStreams);

    var activeStream = activeStreams[stream.id];

    if (!('range' in activeStream)) {
        activeStream.range = {};
    }

    if (stream.datapoints.length === 0) {
        return;
    }

    var firstDatapoint = stream.datapoints[0];
    var lastDatapoint = stream.datapoints[stream.datapoints.length - 1];

    // We go through downsampled timestamps in such order to maximize the range
    var start = $.isPlainObject(firstDatapoint.t) ? firstDefined(firstDatapoint.t, 'a', 'e', 'm', 'z') : firstDatapoint.t;
    var end = $.isPlainObject(lastDatapoint.t) ? firstDefined(lastDatapoint.t, 'z', 'm', 'e', 'a') : lastDatapoint.t;

    if ((typeof start !== 'undefined') && ((typeof activeStream.range.start === 'undefined') || (moment.utc(start).valueOf() < activeStream.range.start))) {
        activeStream.range.start = moment.utc(start).valueOf();
    }
    if ((typeof end !== 'undefined') && ((typeof activeStream.range.end === 'undefined') || (moment.utc(end).valueOf() > activeStream.range.end))) {
        activeStream.range.end = moment.utc(end).valueOf();
    }
}

function initializePlot() {
    new Highcharts.StockChart({
        'chart': {
            'renderTo': 'plot',
            'type': 'spline',
            'zoomType': 'x',
            'borderRadius': 10
        },
        'credits': {
            'enabled': false
        },
        'navigator': {
            'enabled': true,
            'adaptToUpdatedData': false,
            'series': {
                'id': 'navigator',
                // We will add our own series on top of this one and leave this one empty
                'data': []
            }
        },
        'scrollbar': {
            'enabled': true,
            'liveRedraw': false
        },
        'legend': {
            'enabled': true,
            'verticalAlign': 'top',
            'floating': true,
            'padding': 5
        },
        'rangeSelector': {
            'buttonTheme': {
                'width': 50
            },
            'buttons': [
                {
                    'type': 'day',
                    'count': 1,
                    'text': "day"
                },
                {
                    'type': 'week',
                    'count': 1,
                    'text': "week"
                },
                {
                    'type': 'month',
                    'count': 1,
                    'text': "month"
                },
                {
                    'type': 'year',
                    'count': 1,
                    'text': "year"
                },
                {
                    'type': 'all',
                    'text': "all"
                }
            ],
            'selected': 4 // All
        },
        'xAxis': {
            'events': {
                'afterSetExtremes': reloadGraphData
            },
            'ordinal': false,
            'minRange': MAX_POINTS_NUMBER * 1000 // TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
        },
        'yAxis': [],
        'plotOptions': {
            'series': {
                'marker': {
                    'enabled': true,
                    'radius': 3
                },
                'dataGrouping': {
                    'enabled': false
                }
            }
        },
        'series': []
    }, function (p) {
        // Store initialized plot to the global variable
        plot = p;
    });
}

function convertDatapoint(datapoint) {
    var t = moment.utc($.isPlainObject(datapoint.t) ? datapoint.t.m : datapoint.t).valueOf();
    if ($.isPlainObject(datapoint.v)) {
        return {
            'line': [t, datapoint.v.m],
            'range': [t, datapoint.v.l, datapoint.v.u]
        }
    }
    else {
        return {
            'line': [t, datapoint.v],
            'range': [t, datapoint.v, datapoint.v]
        }
    }
}

function convertDatapoints(datapoints) {
    var line = [];
    var range = [];
    $.each(datapoints, function (i, datapoint) {
        datapoint = convertDatapoint(datapoint);
        line.push(datapoint.line);
        range.push(datapoint.range);
    });
    return {
        'line': line,
        'range': range
    };
}

function computeRange(min, max) {
    var range = {
        'granularity': granularities[0]
    };

    if (!$.isNumeric(min) || !$.isNumeric(min)) {
        return range;
    }

    // In JavaScript timestamps are in miliseconds, but server sides uses them in seconds
    range.start = min / 1000;
    range.end = max / 1000;

    var interval = range.end - range.start;

    $.each(granularities, function (i, granularity) {
        if (interval / granularity.duration > MAX_POINTS_NUMBER) {
            return false;
        }
        range.granularity = granularity;
    });

    // We enlarge range for 10 % in each direction
    range.start -= interval * 0.1;
    range.end += interval * 0.1;

    range.start = parseInt(Math.floor(range.start));
    range.end = parseInt(Math.ceil(range.end));

    return range;
}

var loadingShown = 0;

function showLoading() {
    assert(loadingShown >= 0);

    loadingShown++;
    if (loadingShown === 1) {
        plot.showLoading("Loading data from server...");
    }
}

function hideLoading() {
    assert(loadingShown > 0);

    loadingShown--;
    if (loadingShown === 0) {
        plot.hideLoading();
    }
}

function reloadGraphData(event) {
    var range = computeRange(event.min, event.max);
    var streams = $.map(activeStreams, function (stream, id) {
        return stream;
    });
    showLoading();
    async.each(streams, function (stream, cb) {
        $.getJSON(stream.resource_uri, {
            'granularity': range.granularity.name,
            'limit': 10000,
            'start': range.start,
            'end': range.end
        }, function (data, textStatus, jqXHR) {
            assert.equal(data.id, stream.id);

            updateKnownMaxRange(data);

            var datapoints = convertDatapoints(data.datapoints);
            plot.get('line-' + stream.id).setData(datapoints.line);
            plot.get('range-' + stream.id).setData(datapoints.range);

            cb();
        }).fail(function () {
            cb(arguments);
        });
    }, function (err) {
        hideLoading();
    });
}

function addPlotData(stream) {
    var range = computeRange(plot.xAxis[0].userMin, plot.xAxis[0].userMax);
    showLoading();
    $.getJSON(stream.resource_uri, {
        'granularity': range.granularity.name,
        'limit': 10000,
        'start': range.start,
        'end': range.end
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, stream.id);

        updateKnownMaxRange(data);

        var datapoints = convertDatapoints(data.datapoints);

        plot.addAxis({
            'id': 'axis-' + stream.id,
            'title': {
                // TODO: Automatically prefx unit if provided
                'text': [(stream.tags.unit || ''), (stream.tags.unit_description || '')].join(' ')
            },
            'showEmpty': false
        });
        var series = plot.addSeries({
            'id': 'range-' + stream.id,
            'streamId': stream.id, // Our own option
            'name': stream.tags.title,
            'yAxis': 'axis-' + stream.id,
            'type': 'arearange',
            'lineWidth': 0,
            'fillOpacity': 0.3,
            'tooltip': {
                'pointFormat': '{series.name} min/max: {point.low} - {point.high}
',
                'valueDecimals': 3
            },
            'selected': true,
            'events': {
                'legendItemClick': function (e) {
                    e.preventDefault();

                    this.select();

                    // We force mouse leave event to immediately set opacity
                    $(this.legendGroup.element).trigger('mouseleave.highlight');
                }
            },
            'data': datapoints.range
        });
        // Match yAxis title color with series color
        series.yAxis.axisTitle.css({'color': series.color});
        plot.addSeries({
            'id': 'line-' + stream.id,
            'streamId': stream.id, // Our own option
            'name': stream.tags.title,
            'linkedTo': 'range-' + stream.id,
            'color': series.color,
            'yAxis': 'axis-' + stream.id,
            'tooltip': {
                'pointFormat': '{series.name} mean: {point.y}
',
                'valueDecimals': 3
            },
            'data': datapoints.line
        });
        var navigator = plot.get('navigator');
        plot.addAxis(_.extend({}, navigator.yAxis.options, {
            'id': 'navigator-y-axis-' + stream.id
        }));
        plot.addSeries(_.extend({}, navigator.options, {
            'id': 'navigator-' + stream.id,
            'streamId': stream.id, // Our own option
            'color': series.color,
            'data': datapoints.line,
            'yAxis': 'navigator-y-axis-' + stream.id
        }));
        // TODO: Improve/fix this
        var unionExtremes = (plot.scroller && plot.scroller.getUnionExtremes()) || plot.xAxis[0] || {};
        plot.xAxis[0].setExtremes(unionExtremes.dataMin, unionExtremes.dataMax);
    }).always(function () {
        hideLoading();
    });
}

var activeStreams = {};
var plot = null;

$(document).ready(function () {
    $('#streams').empty();

    $.getJSON('/api/v1/stream/', function (data, textStatus, jqXHR) {
        $.each(data.objects, function (i, stream) {
            if (data.id in activeStreams) return;

            $('
').data(stream).html(prettyPrint(stream.tags)).appendTo('#streams').click(function (e) {
                activeStreams[stream.id] = stream;
                addPlotData(stream);
                $(this).remove();
            });
        });
    });

    $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
        console.error(event, jqXHR, ajaxSettings, thrownError);
    });

    initializePlot();
});<commit_msg>Renamed activeStreams to streams.<commit_after>/**
 * Plugin for setting a lower opacity for other series than the one that is hovered over.
 * Additionally, if not hovering over, a lower opacity is set based on series selected status.
 */
(function (Highcharts) {
    function highlightOn(allSeries, currentSeries) {
        return function (e) {
            $.each(allSeries, function (i, series) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                var current = (series === currentSeries) || (series.linkedParent === currentSeries) || (series.options.streamId === currentSeries.options.streamId);
                $.each(['group', 'markerGroup'], function (j, group) {
                    series[group].attr('opacity', current ? 1.0 : 0.25);
                });
                series.chart.legend.colorizeItem(series, current);
            });
        };
    }

    function highlightOff(allSeries, currentSeries) {
        return function (e) {
            $.each(allSeries, function (i, series) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                assert(series.options.streamId);

                var selected = series.selected || _.some(_.filter(allSeries, function (s) {return s.options.streamId === series.options.streamId}), function (s) {return s.selected});

                $.each(['group', 'markerGroup'], function (j, group) {
                    series[group].attr('opacity', selected ? 1.0 : 0.25);
                });
                series.chart.legend.colorizeItem(series, selected);
            });
        };
    }

    // Hovering over the legend
    Highcharts.wrap(Highcharts.Legend.prototype, 'renderItem', function (proceed, currentSeries) {
        proceed.call(this, currentSeries);

        var allSeries = this.chart.series;

        $(currentSeries.legendGroup.element).off('.highlight').on(
            'mouseenter.highlight', highlightOn(allSeries, currentSeries)
        ).on(
            'mouseleave.highlight', highlightOff(allSeries, currentSeries)
        );
    });
}(Highcharts));

// From: http://jsfiddle.net/unLSJ/

function prettyPrint(obj) {
    var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
    return JSON.stringify(obj, null, 3)
        .replace(/&/g, '&').replace(/\\"/g, '"')
        .replace(/</g, '<').replace(/>/g, '>')
        .replace(jsonLine, replacer);
}

function replacer(match, pIndent, pKey, pVal, pEnd) {
    var key = '';
    var val = '';
    var str = '';
    var r = pIndent || '';
    if (pKey)
        r = r + key + pKey.replace(/[": ]/g, '') + ': ';
    if (pVal)
        r = r + (pVal[0] == '"' ? str : val) + pVal + '';
    return r + (pEnd || '');
}

// TODO: This curently does not depend on how many datapoints are really available, so if granularity is seconds, it assumes that every second will have a datapoint
// TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
var MAX_POINTS_NUMBER = 300;

var granularities = [
    {'name': 'days', 'duration': 86400},
    {'name': '6hours', 'duration': 21600},
    {'name': 'hours', 'duration': 3600},
    {'name': '10minutes', 'duration': 600},
    {'name': 'minutes', 'duration': 60},
    {'name': '10seconds', 'duration': 10},
    {'name': 'seconds', 'duration': 1}
];

function firstDefined(obj) {
    for (var i = 1; i < arguments.length; i++) {
        if (typeof obj[arguments[i]] !== 'undefined') {
            return obj[arguments[i]];
        }
    }
}

function updateKnownMaxRange(stream) {
    assert(stream.id in streams);

    var activeStream = streams[stream.id];

    if (!('range' in activeStream)) {
        activeStream.range = {};
    }

    if (stream.datapoints.length === 0) {
        return;
    }

    var firstDatapoint = stream.datapoints[0];
    var lastDatapoint = stream.datapoints[stream.datapoints.length - 1];

    // We go through downsampled timestamps in such order to maximize the range
    var start = $.isPlainObject(firstDatapoint.t) ? firstDefined(firstDatapoint.t, 'a', 'e', 'm', 'z') : firstDatapoint.t;
    var end = $.isPlainObject(lastDatapoint.t) ? firstDefined(lastDatapoint.t, 'z', 'm', 'e', 'a') : lastDatapoint.t;

    if ((typeof start !== 'undefined') && ((typeof activeStream.range.start === 'undefined') || (moment.utc(start).valueOf() < activeStream.range.start))) {
        activeStream.range.start = moment.utc(start).valueOf();
    }
    if ((typeof end !== 'undefined') && ((typeof activeStream.range.end === 'undefined') || (moment.utc(end).valueOf() > activeStream.range.end))) {
        activeStream.range.end = moment.utc(end).valueOf();
    }
}

function initializePlot() {
    new Highcharts.StockChart({
        'chart': {
            'renderTo': 'plot',
            'type': 'spline',
            'zoomType': 'x',
            'borderRadius': 10
        },
        'credits': {
            'enabled': false
        },
        'navigator': {
            'enabled': true,
            'adaptToUpdatedData': false,
            'series': {
                'id': 'navigator',
                // We will add our own series on top of this one and leave this one empty
                'data': []
            }
        },
        'scrollbar': {
            'enabled': true,
            'liveRedraw': false
        },
        'legend': {
            'enabled': true,
            'verticalAlign': 'top',
            'floating': true,
            'padding': 5
        },
        'rangeSelector': {
            'buttonTheme': {
                'width': 50
            },
            'buttons': [
                {
                    'type': 'day',
                    'count': 1,
                    'text': "day"
                },
                {
                    'type': 'week',
                    'count': 1,
                    'text': "week"
                },
                {
                    'type': 'month',
                    'count': 1,
                    'text': "month"
                },
                {
                    'type': 'year',
                    'count': 1,
                    'text': "year"
                },
                {
                    'type': 'all',
                    'text': "all"
                }
            ],
            'selected': 4 // All
        },
        'xAxis': {
            'events': {
                'afterSetExtremes': reloadGraphData
            },
            'ordinal': false,
            'minRange': MAX_POINTS_NUMBER * 1000 // TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
        },
        'yAxis': [],
        'plotOptions': {
            'series': {
                'marker': {
                    'enabled': true,
                    'radius': 3
                },
                'dataGrouping': {
                    'enabled': false
                }
            }
        },
        'series': []
    }, function (p) {
        // Store initialized plot to the global variable
        plot = p;
    });
}

function convertDatapoint(datapoint) {
    var t = moment.utc($.isPlainObject(datapoint.t) ? datapoint.t.m : datapoint.t).valueOf();
    if ($.isPlainObject(datapoint.v)) {
        return {
            'line': [t, datapoint.v.m],
            'range': [t, datapoint.v.l, datapoint.v.u]
        }
    }
    else {
        return {
            'line': [t, datapoint.v],
            'range': [t, datapoint.v, datapoint.v]
        }
    }
}

function convertDatapoints(datapoints) {
    var line = [];
    var range = [];
    $.each(datapoints, function (i, datapoint) {
        datapoint = convertDatapoint(datapoint);
        line.push(datapoint.line);
        range.push(datapoint.range);
    });
    return {
        'line': line,
        'range': range
    };
}

function computeRange(min, max) {
    var range = {
        'granularity': granularities[0]
    };

    if (!$.isNumeric(min) || !$.isNumeric(min)) {
        return range;
    }

    // In JavaScript timestamps are in miliseconds, but server sides uses them in seconds
    range.start = min / 1000;
    range.end = max / 1000;

    var interval = range.end - range.start;

    $.each(granularities, function (i, granularity) {
        if (interval / granularity.duration > MAX_POINTS_NUMBER) {
            return false;
        }
        range.granularity = granularity;
    });

    // We enlarge range for 10 % in each direction
    range.start -= interval * 0.1;
    range.end += interval * 0.1;

    range.start = parseInt(Math.floor(range.start));
    range.end = parseInt(Math.ceil(range.end));

    return range;
}

var loadingShown = 0;

function showLoading() {
    assert(loadingShown >= 0);

    loadingShown++;
    if (loadingShown === 1) {
        plot.showLoading("Loading data from server...");
    }
}

function hideLoading() {
    assert(loadingShown > 0);

    loadingShown--;
    if (loadingShown === 0) {
        plot.hideLoading();
    }
}

function reloadGraphData(event) {
    var range = computeRange(event.min, event.max);
    var streams = $.map(streams, function (stream, id) {
        return stream;
    });
    showLoading();
    async.each(streams, function (stream, cb) {
        $.getJSON(stream.resource_uri, {
            'granularity': range.granularity.name,
            'limit': 10000,
            'start': range.start,
            'end': range.end
        }, function (data, textStatus, jqXHR) {
            assert.equal(data.id, stream.id);

            updateKnownMaxRange(data);

            var datapoints = convertDatapoints(data.datapoints);
            plot.get('line-' + stream.id).setData(datapoints.line);
            plot.get('range-' + stream.id).setData(datapoints.range);

            cb();
        }).fail(function () {
            cb(arguments);
        });
    }, function (err) {
        hideLoading();
    });
}

function addPlotData(stream) {
    var range = computeRange(plot.xAxis[0].userMin, plot.xAxis[0].userMax);
    showLoading();
    $.getJSON(stream.resource_uri, {
        'granularity': range.granularity.name,
        'limit': 10000,
        'start': range.start,
        'end': range.end
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, stream.id);

        updateKnownMaxRange(data);

        var datapoints = convertDatapoints(data.datapoints);

        plot.addAxis({
            'id': 'axis-' + stream.id,
            'title': {
                // TODO: Automatically prefx unit if provided
                'text': [(stream.tags.unit || ''), (stream.tags.unit_description || '')].join(' ')
            },
            'showEmpty': false
        });
        var series = plot.addSeries({
            'id': 'range-' + stream.id,
            'streamId': stream.id, // Our own option
            'name': stream.tags.title,
            'yAxis': 'axis-' + stream.id,
            'type': 'arearange',
            'lineWidth': 0,
            'fillOpacity': 0.3,
            'tooltip': {
                'pointFormat': '{series.name} min/max: {point.low} - {point.high}
',
                'valueDecimals': 3
            },
            'selected': true,
            'events': {
                'legendItemClick': function (e) {
                    e.preventDefault();

                    this.select();

                    // We force mouse leave event to immediately set opacity
                    $(this.legendGroup.element).trigger('mouseleave.highlight');
                }
            },
            'data': datapoints.range
        });
        // Match yAxis title color with series color
        series.yAxis.axisTitle.css({'color': series.color});
        plot.addSeries({
            'id': 'line-' + stream.id,
            'streamId': stream.id, // Our own option
            'name': stream.tags.title,
            'linkedTo': 'range-' + stream.id,
            'color': series.color,
            'yAxis': 'axis-' + stream.id,
            'tooltip': {
                'pointFormat': '{series.name} mean: {point.y}
',
                'valueDecimals': 3
            },
            'data': datapoints.line
        });
        var navigator = plot.get('navigator');
        plot.addAxis(_.extend({}, navigator.yAxis.options, {
            'id': 'navigator-y-axis-' + stream.id
        }));
        plot.addSeries(_.extend({}, navigator.options, {
            'id': 'navigator-' + stream.id,
            'streamId': stream.id, // Our own option
            'color': series.color,
            'data': datapoints.line,
            'yAxis': 'navigator-y-axis-' + stream.id
        }));
        // TODO: Improve/fix this
        var unionExtremes = (plot.scroller && plot.scroller.getUnionExtremes()) || plot.xAxis[0] || {};
        plot.xAxis[0].setExtremes(unionExtremes.dataMin, unionExtremes.dataMax);
    }).always(function () {
        hideLoading();
    });
}

var streams = {};
var plot = null;

$(document).ready(function () {
    $('#streams').empty();

    $.getJSON('/api/v1/stream/', function (data, textStatus, jqXHR) {
        $.each(data.objects, function (i, stream) {
            if (data.id in streams) return;

            $('
').data(stream).html(prettyPrint(stream.tags)).appendTo('#streams').click(function (e) {
                streams[stream.id] = stream;
                addPlotData(stream);
                $(this).remove();
            });
        });
    });

    $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
        console.error(event, jqXHR, ajaxSettings, thrownError);
    });

    initializePlot();
});<|endoftext|>