<commit_before>/*
Copyright (c) 2014, EDINA.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
   list of conditions and the following disclaimer in the documentation and/or
   other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must
   display the following acknowledgement: This product includes software
   developed by the EDINA.
4. Neither the name of the EDINA nor the names of its contributors may be used to
   endorse or promote products derived from this software without specific prior
   written permission.

THIS SOFTWARE IS PROVIDED BY EDINA ''AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL EDINA BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
*/

"use strict";

/* jshint multistr: true */

define(['records', 'map', 'settings', 'ui', 'utils', './pcapi', './login', './upload', './download'], function(// jshint ignore:line
    records, map, settings, ui, utils, pcapi, login, upload, download){

    /**
     * Set up buttons according to whether user if logged in.
     */
    var checkLogin = function(){
        if(login.getUser()){
            showSyncButtons();
        }
        else{
            hideSyncButtons();
        }
    };

    /*
     * Get the list of providers available for the app
     * @param onsuccess a success callback where to pass the list of providers
     * @param onerror an error callback
     */
    var getProviders = function(onsuccess, onerror){
        pcapi.getProviders(function(success, data){
            if(success){
                var providers = [];
                for(var key in data){
                    // Just add selected providers from the app configuration
                    // or all if none was specified
                    var pcapiProviders = utils.getPCAPIProviders();
                    if(pcapiProviders === undefined ||
                       pcapiProviders.indexOf(key) > -1){
                        providers.push(key);
                    }
                }
                if(typeof onsuccess === 'function'){
                    onsuccess(providers);
                }
            }
            else{
                if(typeof onerror === 'function'){
                    onerror();
                }
            }
        });
    };

    /**
     * Hide sync related buttons.
     */
    var hideSyncButtons = function(){
        // Bug 5997 have to use full url due to jqm issue
        $('.sync-login img').attr(
            'src',
            utils.getDocumentBase() + 'plugins/sync/css/images/login-large.png');
        $('.sync-login p').text('Login');

        // hide sync buttons
        $('.sync-show').hide();
    };

    /**
     * Set up records page for syncing.
     */
    var recordsPage = function(){
        /**
         * Show upload and sync button on records page header
         */
        var refreshButtonsState = function(state){
            switch(state){
                case 'loggedin':
                    $('#saved-records-page-header-login-sync')
                        .removeClass('cloud-login')
                        .addClass('cloud-logout');
                    $('#saved-records-page-header-upload').show();
                break;
                case 'loggedout':
                    $('#saved-records-page-header-login-sync')
                        .removeClass('cloud-logout')
                        .addClass('cloud-login');
                    $('#saved-records-page-header-upload').hide();
                break;
            }
        };

        // Sync button: Not in use view: edina/fieldtrip-gb#76
        $(document).off('vmousedown', '#saved-records-page-header-login-sync.cloud-sync');
        $(document).on(
            'vmousedown',
            '#saved-records-page-header-login-sync.cloud-sync',
            function(event){
                event.stopImmediatePropagation();
                if($('#saved-records-page-header-login-sync.cloud-sync').length > 0){
                    sync({
                        div: 'saved-records-sync-popup',
                        callback: function(add, id, annotation){
                            if(add){
                                ui.addAnnotation(id, annotation);
                            }
                            else{
                                // if not add then delete
                                $('#' + id).slideUp('slow');
                            }
                        },
                        complete: function(){
                            $('#saved-annotations-list-list').listview('refresh');
                        }
                    });
                }
        });

        // Login button
        $(document).off('tap', '#saved-records-page-header-login-sync.cloud-login');
        $(document).on(
            'tap',
            '#saved-records-page-header-login-sync.cloud-login',
            function(event){
                event.stopImmediatePropagation();
                var provider = pcapi.getProvider();
                login.loginCloud(provider, function(userId){
                    if(userId){
                        refreshButtonsState('loggedin');
                    }else{
                        refreshButtonsState('loggedout');
                    }
                });
        });

        //Logout button
        $(document).off('tap', '#saved-records-page-header-login-sync.cloud-logout');
        $(document).on(
            'tap',
            '#saved-records-page-header-login-sync.cloud-logout',
            function(event){
                event.stopImmediatePropagation();
                login.logoutCloud();
                refreshButtonsState('loggedout');
        });

        if(login.getUser()){
            refreshButtonsState('loggedin');
        }
        else{
            refreshButtonsState('loggedout');
        }
    };

    /**
     * Login with chosed provider.
     * @param provider
     */
    var selectProvider = function(provider){
        pcapi.setProvider(provider);
        login.loginCloud(provider, function(userId){
            if(userId){
                showSyncButtons();
                $('#home-login-sync-popup').popup('close');
            }
        });
    };

    /**
     * Show buttons for syncing.
     */
    var showSyncButtons = function(){
        // Bug 5997 have to use full url due to jqm issue
        $('.sync-login img').attr(
            'src',
            utils.getDocumentBase() + 'plugins/sync/css/images/logout.png');
        $('.sync-login p').text('Logout');

        // show sync buttons
        $('.sync-show').show();
    };

    /**
     * Sync device with cloud.
     * @param div - The div to append confirmation popup.
     * @param options:
     *   callback - A function that will be executed for each annotation created
     *     or deleted as part of the sync.
     *   complete - A function that will be executed when sync is complete.
     */
    var sync = function(options){
        // to avoid duplication add pop up dynamically
        $('#' + options.div).empty();
        $('#' + options.div).append(
            '
\

All records and forms will be downloaded and all local records will be uploaded (since last sync). To upload records only, use upload button.\

\
               Cancel\
               Continue\

').trigger('create');

        $(document).off('vmousedown', '.sync-confirm');
        $(document).on(
            'vmousedown',
            '.sync-confirm',
            function(event){
                event.preventDefault();
                $('#' + options.div).popup('close');

                // upload unsynced records
                var doUpload = function(){
                    upload.uploadRecords(function(){
                        syncStoreCursor();
                        map.refreshRecords();
                        $.mobile.loading('hide');

                        if(options.complete){
                            options.complete();
                        }
                    });
                };

                // sync uploaded records with dropbox
                if(login.getUser().cursor === undefined){
                    // no cursor found do a full sync
                    download.downloadEditors(function(success){
                        if(success){
                            download.downloadRecords(doUpload, options.callback);
                        }
                        else{
                            $.mobile.loading('hide');
                            utils.inform("Problem syncing editors.");
                        }
                    });
                }
                else{
                    // sync using cursor
                    syncWithCursor(
                        function(success){
                            if(success){
                                doUpload();
                            }
                            else{
                                $.mobile.loading('hide');
                                utils.inform("Problem syncing with cursor.");
                            }
                        },
                        options.callback
                    );
                }
            }
        );

        // timeout allows popup to stay visible on the saved records page on all
        // devices see https://redmine.edina.ac.uk/issues/8228
        setTimeout(function(){
            $('#' + options.div).popup('open');
        }, 750);
    };

    /**
     * Store current dropbox state cursor with cloud login details.
     */
    var syncStoreCursor = function(){
        var userId = login.getUser().id;
        var url = pcapi.getCloudProviderUrl() + '/sync/'+pcapi.getProvider()+'/' + userId;
        $.ajax({
            type: "GET",
            dataType: "json",
            url: url,
            success: function(data){
                console.debug("Save cursor: " + data.cursor);
                login.setCloudLogin(userId, data.cursor);
            },
            error: function(jqXHR, status, error){
                console.error("syncStoreCursor: Problem fetching cursor " + url +
                              " : " + status + " : " + error);
            },
            cache: false
        });
    };

    /**
     * Sync records and editors.
     * @param complete Function executed when sync is complete.
     * @param callback Function executed each time an annotation is added or deleted.
     */
    var syncWithCursor = function(complete, callback) {
        var user = login.getUser();

        // track asynchronous jobs
        var jobs = 0;
        var finished = function(){
            --jobs;
            if(jobs === 0){
                complete(true);
            }
        };

        // retrieve file type and value
        var getDetails = function(path){
            var val;
            var start = path.indexOf('/', 1) + 1;
            var end = path.indexOf('/', start);
            if(end === -1){
                // no end slash its a directory
                val = path.substr(start);
            }
            else{
                val = path.substr(start, end - start);
            }

            return {
                'type': path.substr(1, 7),
                'val': val
            };
        };

        // sync records
        var url = pcapi.getCloudProviderUrl() + '/sync/'+pcapi.getProvider()+'/' +
            user.id + "/" + user.cursor;
        console.debug("Sync download with cursor: " + url);

        $.ajax({
            type: "GET",
            dataType: "json",
            url: url,
            success: function(data){
                utils.printObj(data);
                var rList = [];

                // deleted records and editors
                $.each(data.deleted, function(i, path){
                    var details = getDetails(path);
                    if(details.type === 'records'){
                        if($.inArray(details.val, rList) === -1){
                            var id = records.deleteAnnotationByName(details.val);
                            if(callback){
                                callback(false, id);
                            }
                            rList.push(details.val);
                        }
                    }
                    else if(details.type === 'editors'){
                        records.deleteEditor(details.val);
                    }
                    else{
                        console.warn("No such record type: " + details.type);
                    }
                });

                rList = [];

                // updated records and editors
                $.each(data.updated, function(i, path){
                    var details = getDetails(path);
                    if(details.type === 'records'){
                        // a record update could be the directory, the record json
                        // and assets, this check ensures that the record is only
                        // fetched once
                        if($.inArray(details.val, rList) === -1){
                            // just download the record and assets
                            var record = records.getAnnotationDetails(details.val);

                            ++jobs;
                            rList.push(details.val);
                            download.downloadRecord(
                                details.val,
                                function(success, id, annotation){
                                    if(success && record === undefined && callback){
                                        // record undefined means a new record has been
                                        // downloaded, perhaps as a rename
                                        callback(true, id, annotation);
                                    }

                                    finished();
                                },
                                record
                            );
                        }
                    }
                    else if(details.type === 'editors'){
                        ++jobs;
                        download.downloadEditor(details.val, function(){
                            finished();
                        });
                    }
                    else{
                        console.warn("No such record type:" + details.type);
                    }
                });

                if(jobs === 0){
                    complete(true);
                }
            },
            error: function(jqXHR, status, error){
                console.error("SyncWithCursor: Problem fetching " + url + " : " +
                              status + " : " + error);
                complete(false);
            },
            cache: false
        });
    };

    var root;
    if(utils.isMobileDevice()){
        // check settings first for defined pcapi root url
        root = settings.get("pcapi-url");
        if(root === undefined){
            root = utils.getPCAPIURL();
        }
    }
    else{
        root = 'http://' + location.hostname;
        if(location.port){
            root += ':' + location.port;
        }
    }
    pcapi.init({"url": root, "version": utils.getPCAPIVersion()});

    login.checkLogin(function(userId){
        if(userId){
            showSyncButtons();
        }
    });

    // listen on home page
    $(document).on('_pageshow', '#home-page', checkLogin);

    // listen on saved records page
    $(document).on('_pageshow', '#saved-records-page', recordsPage);

    // listen on any page with class sync-page
    $(document).on('_pageshow', '.sync-page', checkLogin);

    $(document).on('vclick', '#home-content-login', function(){
        var icon = $('#home-content-login img').attr('src');
        icon = icon.substr(icon.lastIndexOf('/') + 1);

        if(icon === 'login-large.png'){
            var $loginPopup = $('#home-login-sync-popup');

            var onsuccess = function(providers){
                // If there is only one non-'local' provider login with that one
                if(providers.length == 1 && providers[0] != 'local'){
                    selectProvider(providers[0]);
                }
                else{
                    var html = [];
                    for(var i=0; i<providers.length; i++){
                        var provider = providers[i];
                        if(provider == 'local'){
                            html.push('
'+provider+'
');
                            html.push('
Username:');
                            html.push('<input type="text" name="login-username" id="login-username" value="cobweb@cobweb.ed.ac.uk">');
                            html.push('<input type="button" id="local-login" value="Login" data-theme="b">
');
                        }
                        else{
                            html.push('
'+provider+'
');
                        }
                    }
                    $("#list-providers").html(html.join(""));
                    $("#list-providers").listview('refresh');
                    $loginPopup.popup('open');
                }
            };

            var onerror = function(){
                console.debug('Error querying the providers');
            };

            getProviders(onsuccess, onerror);
        }
        else {
            login.logoutCloud();
            hideSyncButtons();
        }
    });

    $(document).on('vclick', '.choose-provider', function(event){
        selectProvider($(event.currentTarget).text());
    });

    $(document).on(
        'vclick',
        '.sync-upload-button',
        function(event){
            event.preventDefault();
            utils.showPageLoadingMsg('Upload Records ...');
            upload.uploadRecords(function(){
                $.mobile.loading('hide');
                $.mobile.changePage("saved-records.html");
            });
        }
    );
    $(document).on(
        'vclick',
        '.sync-download-button',
        function(){
            utils.showPageLoadingMsg('Download Editors ...');
            download.downloadEditors(function(){
                $.mobile.loading('hide');
                $.mobile.changePage('capture.html');
            });
        }
    );
    $(document).on(
        'vclick',
        '#home-content-sync',
        function(event){
            //event.preventDefault();
            sync({
                div: 'home-sync-popup',
                complete: function(){
                    $.mobile.changePage('capture.html');
                }
            });
        }
    );

    $(document).on('change', '#settings-pcapi-url', function(){
        pcapi.setCloudProviderUrl(
            $('#settings-pcapi-url option:selected').val());
    });

    $('head').prepend('<link rel="stylesheet" href="plugins/sync/css/style.css" type="text/css" />');
});
<commit_msg>edina/fieldtrip-osm#18 remove ui as sync dependency<commit_after>/*
Copyright (c) 2014, EDINA.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
   list of conditions and the following disclaimer in the documentation and/or
   other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must
   display the following acknowledgement: This product includes software
   developed by the EDINA.
4. Neither the name of the EDINA nor the names of its contributors may be used to
   endorse or promote products derived from this software without specific prior
   written permission.

THIS SOFTWARE IS PROVIDED BY EDINA ''AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL EDINA BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
*/

"use strict";

/* jshint multistr: true */

define(['records', 'map', 'settings', 'utils', './pcapi', './login', './upload', './download'], function(// jshint ignore:line
    records, map, settings, utils, pcapi, login, upload, download){

    /**
     * Set up buttons according to whether user if logged in.
     */
    var checkLogin = function(){
        if(login.getUser()){
            showSyncButtons();
        }
        else{
            hideSyncButtons();
        }
    };

    /*
     * Get the list of providers available for the app
     * @param onsuccess a success callback where to pass the list of providers
     * @param onerror an error callback
     */
    var getProviders = function(onsuccess, onerror){
        pcapi.getProviders(function(success, data){
            if(success){
                var providers = [];
                for(var key in data){
                    // Just add selected providers from the app configuration
                    // or all if none was specified
                    var pcapiProviders = utils.getPCAPIProviders();
                    if(pcapiProviders === undefined ||
                       pcapiProviders.indexOf(key) > -1){
                        providers.push(key);
                    }
                }
                if(typeof onsuccess === 'function'){
                    onsuccess(providers);
                }
            }
            else{
                if(typeof onerror === 'function'){
                    onerror();
                }
            }
        });
    };

    /**
     * Hide sync related buttons.
     */
    var hideSyncButtons = function(){
        // Bug 5997 have to use full url due to jqm issue
        $('.sync-login img').attr(
            'src',
            utils.getDocumentBase() + 'plugins/sync/css/images/login-large.png');
        $('.sync-login p').text('Login');

        // hide sync buttons
        $('.sync-show').hide();
    };

    /**
     * Set up records page for syncing.
     */
    var recordsPage = function(){
        /**
         * Show upload and sync button on records page header
         */
        var refreshButtonsState = function(state){
            switch(state){
                case 'loggedin':
                    $('#saved-records-page-header-login-sync')
                        .removeClass('cloud-login')
                        .addClass('cloud-logout');
                    $('#saved-records-page-header-upload').show();
                break;
                case 'loggedout':
                    $('#saved-records-page-header-login-sync')
                        .removeClass('cloud-logout')
                        .addClass('cloud-login');
                    $('#saved-records-page-header-upload').hide();
                break;
            }
        };

        // Sync button: Not used by ftgb: edina/fieldtrip-gb#76
        $(document).off('vmousedown', '#saved-records-page-header-login-sync.cloud-sync');
        $(document).on(
            'vmousedown',
            '#saved-records-page-header-login-sync.cloud-sync',
            function(event){
                event.stopImmediatePropagation();
                if($('#saved-records-page-header-login-sync.cloud-sync').length > 0){
                    sync({
                        div: 'saved-records-sync-popup',
                        callback: function(add, id, annotation){
                            if(add){
                                records.addAnnotationToSavedRecords(id, annotation);
                            }
                            else{
                                // if not add then delete
                                $('#' + id).slideUp('slow');
                            }
                        },
                        complete: function(){
                            $('#saved-annotations-list-list').listview('refresh');
                        }
                    });
                }
        });

        // Login button
        $(document).off('tap', '#saved-records-page-header-login-sync.cloud-login');
        $(document).on(
            'tap',
            '#saved-records-page-header-login-sync.cloud-login',
            function(event){
                event.stopImmediatePropagation();
                var provider = pcapi.getProvider();
                login.loginCloud(provider, function(userId){
                    if(userId){
                        refreshButtonsState('loggedin');
                    }else{
                        refreshButtonsState('loggedout');
                    }
                });
        });

        //Logout button
        $(document).off('tap', '#saved-records-page-header-login-sync.cloud-logout');
        $(document).on(
            'tap',
            '#saved-records-page-header-login-sync.cloud-logout',
            function(event){
                event.stopImmediatePropagation();
                login.logoutCloud();
                refreshButtonsState('loggedout');
        });

        if(login.getUser()){
            refreshButtonsState('loggedin');
        }
        else{
            refreshButtonsState('loggedout');
        }
    };

    /**
     * Login with chosed provider.
     * @param provider
     */
    var selectProvider = function(provider){
        pcapi.setProvider(provider);
        login.loginCloud(provider, function(userId){
            if(userId){
                showSyncButtons();
                $('#home-login-sync-popup').popup('close');
            }
        });
    };

    /**
     * Show buttons for syncing.
     */
    var showSyncButtons = function(){
        // Bug 5997 have to use full url due to jqm issue
        $('.sync-login img').attr(
            'src',
            utils.getDocumentBase() + 'plugins/sync/css/images/logout.png');
        $('.sync-login p').text('Logout');

        // show sync buttons
        $('.sync-show').show();
    };

    /**
     * Sync device with cloud.
     * @param div - The div to append confirmation popup.
     * @param options:
     *   callback - A function that will be executed for each annotation created
     *     or deleted as part of the sync.
     *   complete - A function that will be executed when sync is complete.
     */
    var sync = function(options){
        // to avoid duplication add pop up dynamically
        $('#' + options.div).empty();
        $('#' + options.div).append(
            '
\

All records and forms will be downloaded and all local records will be uploaded (since last sync). To upload records only, use upload button.\

\
               Cancel\
               Continue\

').trigger('create');

        $(document).off('vmousedown', '.sync-confirm');
        $(document).on(
            'vmousedown',
            '.sync-confirm',
            function(event){
                event.preventDefault();
                $('#' + options.div).popup('close');

                // upload unsynced records
                var doUpload = function(){
                    upload.uploadRecords(function(){
                        syncStoreCursor();
                        map.refreshRecords();
                        $.mobile.loading('hide');

                        if(options.complete){
                            options.complete();
                        }
                    });
                };

                // sync uploaded records with dropbox
                if(login.getUser().cursor === undefined){
                    // no cursor found do a full sync
                    download.downloadEditors(function(success){
                        if(success){
                            download.downloadRecords(doUpload, options.callback);
                        }
                        else{
                            $.mobile.loading('hide');
                            utils.inform("Problem syncing editors.");
                        }
                    });
                }
                else{
                    // sync using cursor
                    syncWithCursor(
                        function(success){
                            if(success){
                                doUpload();
                            }
                            else{
                                $.mobile.loading('hide');
                                utils.inform("Problem syncing with cursor.");
                            }
                        },
                        options.callback
                    );
                }
            }
        );

        // timeout allows popup to stay visible on the saved records page on all
        // devices see https://redmine.edina.ac.uk/issues/8228
        setTimeout(function(){
            $('#' + options.div).popup('open');
        }, 750);
    };

    /**
     * Store current dropbox state cursor with cloud login details.
     */
    var syncStoreCursor = function(){
        var userId = login.getUser().id;
        var url = pcapi.getCloudProviderUrl() + '/sync/'+pcapi.getProvider()+'/' + userId;
        $.ajax({
            type: "GET",
            dataType: "json",
            url: url,
            success: function(data){
                console.debug("Save cursor: " + data.cursor);
                login.setCloudLogin(userId, data.cursor);
            },
            error: function(jqXHR, status, error){
                console.error("syncStoreCursor: Problem fetching cursor " + url +
                              " : " + status + " : " + error);
            },
            cache: false
        });
    };

    /**
     * Sync records and editors.
     * @param complete Function executed when sync is complete.
     * @param callback Function executed each time an annotation is added or deleted.
     */
    var syncWithCursor = function(complete, callback) {
        var user = login.getUser();

        // track asynchronous jobs
        var jobs = 0;
        var finished = function(){
            --jobs;
            if(jobs === 0){
                complete(true);
            }
        };

        // retrieve file type and value
        var getDetails = function(path){
            var val;
            var start = path.indexOf('/', 1) + 1;
            var end = path.indexOf('/', start);
            if(end === -1){
                // no end slash its a directory
                val = path.substr(start);
            }
            else{
                val = path.substr(start, end - start);
            }

            return {
                'type': path.substr(1, 7),
                'val': val
            };
        };

        // sync records
        var url = pcapi.getCloudProviderUrl() + '/sync/'+pcapi.getProvider()+'/' +
            user.id + "/" + user.cursor;
        console.debug("Sync download with cursor: " + url);

        $.ajax({
            type: "GET",
            dataType: "json",
            url: url,
            success: function(data){
                utils.printObj(data);
                var rList = [];

                // deleted records and editors
                $.each(data.deleted, function(i, path){
                    var details = getDetails(path);
                    if(details.type === 'records'){
                        if($.inArray(details.val, rList) === -1){
                            var id = records.deleteAnnotationByName(details.val);
                            if(callback){
                                callback(false, id);
                            }
                            rList.push(details.val);
                        }
                    }
                    else if(details.type === 'editors'){
                        records.deleteEditor(details.val);
                    }
                    else{
                        console.warn("No such record type: " + details.type);
                    }
                });

                rList = [];

                // updated records and editors
                $.each(data.updated, function(i, path){
                    var details = getDetails(path);
                    if(details.type === 'records'){
                        // a record update could be the directory, the record json
                        // and assets, this check ensures that the record is only
                        // fetched once
                        if($.inArray(details.val, rList) === -1){
                            // just download the record and assets
                            var record = records.getAnnotationDetails(details.val);

                            ++jobs;
                            rList.push(details.val);
                            download.downloadRecord(
                                details.val,
                                function(success, id, annotation){
                                    if(success && record === undefined && callback){
                                        // record undefined means a new record has been
                                        // downloaded, perhaps as a rename
                                        callback(true, id, annotation);
                                    }

                                    finished();
                                },
                                record
                            );
                        }
                    }
                    else if(details.type === 'editors'){
                        ++jobs;
                        download.downloadEditor(details.val, function(){
                            finished();
                        });
                    }
                    else{
                        console.warn("No such record type:" + details.type);
                    }
                });

                if(jobs === 0){
                    complete(true);
                }
            },
            error: function(jqXHR, status, error){
                console.error("SyncWithCursor: Problem fetching " + url + " : " +
                              status + " : " + error);
                complete(false);
            },
            cache: false
        });
    };

    var root;
    if(utils.isMobileDevice()){
        // check settings first for defined pcapi root url
        root = settings.get("pcapi-url");
        if(root === undefined){
            root = utils.getPCAPIURL();
        }
    }
    else{
        root = 'http://' + location.hostname;
        if(location.port){
            root += ':' + location.port;
        }
    }
    pcapi.init({"url": root, "version": utils.getPCAPIVersion()});

    login.checkLogin(function(userId){
        if(userId){
            showSyncButtons();
        }
    });

    // listen on home page
    $(document).on('_pageshow', '#home-page', checkLogin);

    // listen on saved records page
    $(document).on('_pageshow', '#saved-records-page', recordsPage);

    // listen on any page with class sync-page
    $(document).on('_pageshow', '.sync-page', checkLogin);

    $(document).on('vclick', '#home-content-login', function(){
        var icon = $('#home-content-login img').attr('src');
        icon = icon.substr(icon.lastIndexOf('/') + 1);

        if(icon === 'login-large.png'){
            var $loginPopup = $('#home-login-sync-popup');

            var onsuccess = function(providers){
                // If there is only one non-'local' provider login with that one
                if(providers.length == 1 && providers[0] != 'local'){
                    selectProvider(providers[0]);
                }
                else{
                    var html = [];
                    for(var i=0; i<providers.length; i++){
                        var provider = providers[i];
                        if(provider == 'local'){
                            html.push('
'+provider+'
');
                            html.push('
Username:');
                            html.push('<input type="text" name="login-username" id="login-username" value="cobweb@cobweb.ed.ac.uk">');
                            html.push('<input type="button" id="local-login" value="Login" data-theme="b">
');
                        }
                        else{
                            html.push('
'+provider+'
');
                        }
                    }
                    $("#list-providers").html(html.join(""));
                    $("#list-providers").listview('refresh');
                    $loginPopup.popup('open');
                }
            };

            var onerror = function(){
                console.debug('Error querying the providers');
            };

            getProviders(onsuccess, onerror);
        }
        else {
            login.logoutCloud();
            hideSyncButtons();
        }
    });

    $(document).on('vclick', '.choose-provider', function(event){
        selectProvider($(event.currentTarget).text());
    });

    $(document).on(
        'vclick',
        '.sync-upload-button',
        function(event){
            event.preventDefault();
            utils.showPageLoadingMsg('Upload Records ...');
            upload.uploadRecords(function(){
                $.mobile.loading('hide');
                $.mobile.changePage("saved-records.html");
            });
        }
    );
    $(document).on(
        'vclick',
        '.sync-download-button',
        function(){
            utils.showPageLoadingMsg('Download Editors ...');
            download.downloadEditors(function(){
                $.mobile.loading('hide');
                $.mobile.changePage('capture.html');
            });
        }
    );
    $(document).on(
        'vclick',
        '#home-content-sync',
        function(event){
            //event.preventDefault();
            sync({
                div: 'home-sync-popup',
                complete: function(){
                    $.mobile.changePage('capture.html');
                }
            });
        }
    );

    $(document).on('change', '#settings-pcapi-url', function(){
        pcapi.setCloudProviderUrl(
            $('#settings-pcapi-url option:selected').val());
    });

    $('head').prepend('<link rel="stylesheet" href="plugins/sync/css/style.css" type="text/css" />');
});
<|endoftext|>


<commit_before>(function () {
  'use strict';

  var dopplerRelayModule = angular
    .module('dopplerRelay', [
        'ngRoute',
        'ngSanitize',
        'pascalprecht.translate',
        'ngAnimate',
        'angular-jwt',
        'autofill-directive',
        'angularModalService',
        'angular-click-outside',
        'duScroll',
        'angularMoment',
        'daterangepicker',
        'ui.select',
        'slugifier',
        '720kb.tooltips',
        'rzModule',
        'ui.mask'
    ])
    .filter('escapeURI', function(){
      return window.encodeURIComponent;
    })
    .config([
      '$routeProvider',
      '$translateProvider',
      '$locationProvider',
      '$httpProvider',
      'jwtInterceptorProvider',
      'uiSelectConfig',
      'tooltipsConfProvider',
      '$provide',
      function (
        $routeProvider,
        $translateProvider,
        $locationProvider,
        $httpProvider,
        jwtInterceptorProvider,
        uiSelectConfig,
        tooltipsConfProvider,
        $provide) {

      function makeStateful($delegate) {
        $delegate.$stateful = true;
        return $delegate;
      }

      $provide.decorator('dateFilter', ['$delegate', makeStateful]);
      $provide.decorator('numberFilter', ['$delegate', makeStateful]);
      $provide.decorator('currencyFilter', ['$delegate', makeStateful]);

      //  $locationProvider.html5Mode(true); //this apply HTML5MODE
      uiSelectConfig.theme = 'selectize';
      tooltipsConfProvider.configure({
        'smart':true,
        'size':'large',
        'speed': 'fast',
        'tooltipTemplateUrlCache': true
      });

      $routeProvider
        .when('/login', {
          templateUrl: 'partials/login/login.html',
          controller: 'LoginCtrl'
        })
        .when('/dashboard', {
          templateUrl: 'partials/dashboard/dashboard.html',
          controller: 'DashboardCtrl'
        })
        .when('/templates', {
          templateUrl: 'partials/templates/templates.html',
          controller: 'TemplatesCtrl'
        })
        .when('/templates/new', {
          templateUrl: 'partials/templates/template.html',
          controller: 'TemplateCtrl'
        })
        .when('/templates/:templateId', {
          templateUrl: 'partials/templates/template.html',
          controller: 'TemplateCtrl'
        })
        .when('/reports', {
          templateUrl: 'partials/reports/reports.html',
          controller: 'ReportsCtrl'
        })
        .when('/reports/downloads', {
          templateUrl: 'partials/downloads/downloads.html',
          controller: 'DownloadsCtrl'
        })
        .when('/signup/confirmation', {
          templateUrl: 'partials/signup/confirmationPage.html',
          controller: 'ConfirmationCtrl',
          controllerAs: 'vm'
        })
        .when('/signup/error', {
          templateUrl: 'partials/signup/error.html'
        })
        .when('/reset-password', {
          templateUrl: 'partials/resetPassword/resetPassword.html',
          controller: 'ResetPasswordCtrl',
          controllerAs: 'vm'
        })
        .when('/temporal-token-error', {
          templateUrl: 'partials/temporal-token-error.html'
        })
        .when('/settings/connection-settings', {
          templateUrl: 'partials/settings/connection-settings.html',
          controller: 'SettingsCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/domain-manager', {
          templateUrl: 'partials/settings/domain-manager.html',
          controller: 'DomainManagerCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/dkim-configuration-help', {
          templateUrl: 'partials/settings/dkim-configuration-help.html',
          controller: 'dKimConfigCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/my-profile', {
          templateUrl: 'partials/settings/my-profile.html',
          controller: 'MyProfileCtrl',
          controllerAs: 'vm'
        })
        .when('/signup/registration', {
          templateUrl: 'partials/signup/registration.html',
          controller: 'RegistrationCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/my-plan', {
          templateUrl: 'partials/settings/my-plan.html',
          controller: 'PlanCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/billing', {
          templateUrl: 'partials/settings/billing.html',
          controller: 'BillingCtrl',
          controllerAs: 'vm'
        })
        .otherwise({
          redirectTo: '/reports'
        });

      $translateProvider
        .translations('en', window["relay-translation-en"])
        .translations('es', window["relay-translation-es"])
        .preferredLanguage('en')
        .useSanitizeValueStrategy('sanitizeParameters');

      jwtInterceptorProvider.tokenGetter = ['auth', function (auth) { return auth.getApiToken(); }];

      $httpProvider.interceptors.push('jwtInterceptor');

      $httpProvider.interceptors.push('errorHandlerInterceptor');

    }]);

  dopplerRelayModule.run([
    '$rootScope',
    'auth',
    '$location',
    '$translate',
    'jwtHelper',
    '$locale',
    function (
      $rootScope,
      auth,
      $location,
      $translate,
      jwtHelper,
      $locale) {

    function applyCultureFormats() {
      $locale.NUMBER_FORMATS.DECIMAL_SEP = $translate.instant("CULTURE_FORMATS.NUMBER_FORMATS.DECIMAL_SEP");
      $locale.NUMBER_FORMATS.GROUP_SEP = $translate.instant("CULTURE_FORMATS.NUMBER_FORMATS.GROUP_SEP");
    }

    applyCultureFormats();

    $rootScope.$on('$translateChangeEnd', applyCultureFormats);

    $rootScope.$on('$locationChangeStart', function () {
      var queryParams = $location.search();

      var queryLang = queryParams['lang'];
      if (queryLang) {
        $location.search('lang', null);
        $translate.use(queryLang);
      }

      var queryTemporalToken = queryParams['temporalToken'];
      if (queryTemporalToken) {
        $location.search('temporalToken', null);
        try {
          if (jwtHelper.isTokenExpired(queryTemporalToken)) {
            $location.path('/temporal-token-error');
            return;
          }
          auth.saveToken(queryTemporalToken, true);
        } catch (error) {
          $location.path('/temporal-token-error');
        }
      }

      verifyAuthorization($location, auth);
    });
  }]);

  //// Uncomment if you want to mock HTTP calls
  //dopplerRelayModule.requires.push('ngMockE2E');
  //dopplerRelayModule.run(['$httpBackend', function ($httpBackend) {
  //  $httpBackend.whenGET(/(\.htm|\.html)$/).passThrough();
  //  $httpBackend.whenPUT(/\/user\/password\/recover/).respond(function (method, url, data) {
  //    console.log("** PUT user/password/recover **");
  //    return [200, {}];
  //  });
  //}]);

  function verifyAuthorization($location, auth) {
    var openForAllUrls = ['/signup/error', '/temporal-token-error', '/dkim-configuration-tutorial'];
    var requireLogoutUrls = ['/signup/confirmation', '/login'];
    var requireTemporalAuthUrls = ['/reset-password', '/signup/registration'];

    // TODO: optimize it
    var currentPath = $location.$$path;
    var userIsAuthed = auth.isAuthed();
    var userIsAuthedTemporarily = userIsAuthed && auth.isTemporarilyAuthed();
    var pageOpenForAll = openForAllUrls.includes(currentPath);
    var pageRequireLogout = requireLogoutUrls.includes(currentPath);
    var pageRequireTemporalAuth = requireTemporalAuthUrls.includes(currentPath);

    if (pageOpenForAll) {
      // Idea: it is possible to setup a flag on redirection, to avoid to enter to this page directly
      return;
    }

    if (pageRequireLogout) {
      // It is not necessary to test if userIsAuthed
      auth.logOut();
      return;
    }

    if (pageRequireTemporalAuth && !userIsAuthedTemporarily) {
      $location.path('/temporal-token-error');
      return;
    }

    if (!userIsAuthed || (!pageRequireTemporalAuth && userIsAuthedTemporarily)) {
      $location.path('/login');
      return;
    }
  }

  typekitLoad('hdx0jiv', function () {
    // Typekit failed to download or initialize
  });

})();
<commit_msg>DR-1253 - Reuse our locale objects to merge culture formats into $locale<commit_after>(function () {
  'use strict';

  function getLocale(lang) {
    return window['relay-translation-' + lang];
  }

  var dopplerRelayModule = angular
    .module('dopplerRelay', [
        'ngRoute',
        'ngSanitize',
        'pascalprecht.translate',
        'ngAnimate',
        'angular-jwt',
        'autofill-directive',
        'angularModalService',
        'angular-click-outside',
        'duScroll',
        'angularMoment',
        'daterangepicker',
        'ui.select',
        'slugifier',
        '720kb.tooltips',
        'rzModule',
        'ui.mask'
    ])
    .filter('escapeURI', function(){
      return window.encodeURIComponent;
    })
    .config([
      '$routeProvider',
      '$translateProvider',
      '$locationProvider',
      '$httpProvider',
      'jwtInterceptorProvider',
      'uiSelectConfig',
      'tooltipsConfProvider',
      '$provide',
      function (
        $routeProvider,
        $translateProvider,
        $locationProvider,
        $httpProvider,
        jwtInterceptorProvider,
        uiSelectConfig,
        tooltipsConfProvider,
        $provide) {

      function makeStateful($delegate) {
        $delegate.$stateful = true;
        return $delegate;
      }

      $provide.decorator('dateFilter', ['$delegate', makeStateful]);
      $provide.decorator('numberFilter', ['$delegate', makeStateful]);
      $provide.decorator('currencyFilter', ['$delegate', makeStateful]);

      //  $locationProvider.html5Mode(true); //this apply HTML5MODE
      uiSelectConfig.theme = 'selectize';
      tooltipsConfProvider.configure({
        'smart':true,
        'size':'large',
        'speed': 'fast',
        'tooltipTemplateUrlCache': true
      });

      $routeProvider
        .when('/login', {
          templateUrl: 'partials/login/login.html',
          controller: 'LoginCtrl'
        })
        .when('/dashboard', {
          templateUrl: 'partials/dashboard/dashboard.html',
          controller: 'DashboardCtrl'
        })
        .when('/templates', {
          templateUrl: 'partials/templates/templates.html',
          controller: 'TemplatesCtrl'
        })
        .when('/templates/new', {
          templateUrl: 'partials/templates/template.html',
          controller: 'TemplateCtrl'
        })
        .when('/templates/:templateId', {
          templateUrl: 'partials/templates/template.html',
          controller: 'TemplateCtrl'
        })
        .when('/reports', {
          templateUrl: 'partials/reports/reports.html',
          controller: 'ReportsCtrl'
        })
        .when('/reports/downloads', {
          templateUrl: 'partials/downloads/downloads.html',
          controller: 'DownloadsCtrl'
        })
        .when('/signup/confirmation', {
          templateUrl: 'partials/signup/confirmationPage.html',
          controller: 'ConfirmationCtrl',
          controllerAs: 'vm'
        })
        .when('/signup/error', {
          templateUrl: 'partials/signup/error.html'
        })
        .when('/reset-password', {
          templateUrl: 'partials/resetPassword/resetPassword.html',
          controller: 'ResetPasswordCtrl',
          controllerAs: 'vm'
        })
        .when('/temporal-token-error', {
          templateUrl: 'partials/temporal-token-error.html'
        })
        .when('/settings/connection-settings', {
          templateUrl: 'partials/settings/connection-settings.html',
          controller: 'SettingsCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/domain-manager', {
          templateUrl: 'partials/settings/domain-manager.html',
          controller: 'DomainManagerCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/dkim-configuration-help', {
          templateUrl: 'partials/settings/dkim-configuration-help.html',
          controller: 'dKimConfigCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/my-profile', {
          templateUrl: 'partials/settings/my-profile.html',
          controller: 'MyProfileCtrl',
          controllerAs: 'vm'
        })
        .when('/signup/registration', {
          templateUrl: 'partials/signup/registration.html',
          controller: 'RegistrationCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/my-plan', {
          templateUrl: 'partials/settings/my-plan.html',
          controller: 'PlanCtrl',
          controllerAs: 'vm'
        })
        .when('/settings/billing', {
          templateUrl: 'partials/settings/billing.html',
          controller: 'BillingCtrl',
          controllerAs: 'vm'
        })
        .otherwise({
          redirectTo: '/reports'
        });

      $translateProvider
        .translations('en', getLocale('en'))
        .translations('es', getLocale('es'))
        .preferredLanguage('en')
        .useSanitizeValueStrategy('sanitizeParameters');

      jwtInterceptorProvider.tokenGetter = ['auth', function (auth) { return auth.getApiToken(); }];

      $httpProvider.interceptors.push('jwtInterceptor');

      $httpProvider.interceptors.push('errorHandlerInterceptor');

    }]);

  dopplerRelayModule.run([
    '$rootScope',
    'auth',
    '$location',
    '$translate',
    'jwtHelper',
    '$locale',
    function (
      $rootScope,
      auth,
      $location,
      $translate,
      jwtHelper,
      $locale) {

    function applyCultureFormats() {
      var locale = getLocale($translate.use());
      angular.merge($locale, locale['CULTURE_FORMATS']);
    }

    applyCultureFormats();

    $rootScope.$on('$translateChangeEnd', applyCultureFormats);

    $rootScope.$on('$locationChangeStart', function () {
      var queryParams = $location.search();

      var queryLang = queryParams['lang'];
      if (queryLang) {
        $location.search('lang', null);
        $translate.use(queryLang);
      }

      var queryTemporalToken = queryParams['temporalToken'];
      if (queryTemporalToken) {
        $location.search('temporalToken', null);
        try {
          if (jwtHelper.isTokenExpired(queryTemporalToken)) {
            $location.path('/temporal-token-error');
            return;
          }
          auth.saveToken(queryTemporalToken, true);
        } catch (error) {
          $location.path('/temporal-token-error');
        }
      }

      verifyAuthorization($location, auth);
    });
  }]);

  //// Uncomment if you want to mock HTTP calls
  //dopplerRelayModule.requires.push('ngMockE2E');
  //dopplerRelayModule.run(['$httpBackend', function ($httpBackend) {
  //  $httpBackend.whenGET(/(\.htm|\.html)$/).passThrough();
  //  $httpBackend.whenPUT(/\/user\/password\/recover/).respond(function (method, url, data) {
  //    console.log("** PUT user/password/recover **");
  //    return [200, {}];
  //  });
  //}]);

  function verifyAuthorization($location, auth) {
    var openForAllUrls = ['/signup/error', '/temporal-token-error', '/dkim-configuration-tutorial'];
    var requireLogoutUrls = ['/signup/confirmation', '/login'];
    var requireTemporalAuthUrls = ['/reset-password', '/signup/registration'];

    // TODO: optimize it
    var currentPath = $location.$$path;
    var userIsAuthed = auth.isAuthed();
    var userIsAuthedTemporarily = userIsAuthed && auth.isTemporarilyAuthed();
    var pageOpenForAll = openForAllUrls.includes(currentPath);
    var pageRequireLogout = requireLogoutUrls.includes(currentPath);
    var pageRequireTemporalAuth = requireTemporalAuthUrls.includes(currentPath);

    if (pageOpenForAll) {
      // Idea: it is possible to setup a flag on redirection, to avoid to enter to this page directly
      return;
    }

    if (pageRequireLogout) {
      // It is not necessary to test if userIsAuthed
      auth.logOut();
      return;
    }

    if (pageRequireTemporalAuth && !userIsAuthedTemporarily) {
      $location.path('/temporal-token-error');
      return;
    }

    if (!userIsAuthed || (!pageRequireTemporalAuth && userIsAuthedTemporarily)) {
      $location.path('/login');
      return;
    }
  }

  typekitLoad('hdx0jiv', function () {
    // Typekit failed to download or initialize
  });

})();
<|endoftext|>


<commit_before>// require jquery/min/2.2.0
// require ./array

"use strict";

var
    jQuery;

(function ($) {

	$.fn.extend({

        centralizeAt: function (obj) {
            var
                self = $(this);

            self.css("position", "absolute");

            self.css({
                top: ($(obj).outerHeight() / 2) - (self.outerHeight() / 2) + "px",
                left: ($(obj).outerWidth() / 2) - (self.outerWidth() / 2) + "px"
            });

            return self;
        },

        findHasNotAttr: function (query, attr) {
            var
                self = $(this);

            return self.find(query + ":not([" + attr + "])");
        },

        findByAttr: function (query, attr, value) {
            return $(this).find(query + "[" + attr + "=\"" + value + "\"]");
        },

        textsAsArray: function () {
            var
                arr = [];

            $(this).each(function (i, obj) {
                arr.push(obj.textContent.trim());
            });

            return arr;
        },

        findByContentText: function (text) {
            return $(this).contents().filter(function () {
                return $(this).text().trim() === text.trim();
            });
        },

        sortByTextContent: function (order) {
            var
                self = $(this),
                arr = [],
                asArr = [];

            order = order.toLocaleLowerCase();
            asArr = $(this).textsAsArray();

            if (order === "asc") {
                asArr = asArr.asc();
            } else if (order === "desc") {
                asArr = asArr.desc();
            }

            asArr.map(function (text) {
                arr.push(self.parent().findByContentText(text).get(0));
            });

            return $(arr);
        }

    });

}(jQuery));
<commit_msg>rename jquery extenion method<commit_after>// require jquery/min/2.2.0
// require ./array

"use strict";

var
    jQuery;

(function ($) {

	$.fn.extend({

        centralizeAt: function (obj) {
            var
                self = $(this);

            self.css("position", "absolute");

            self.css({
                top: ($(obj).outerHeight() / 2) - (self.outerHeight() / 2) + "px",
                left: ($(obj).outerWidth() / 2) - (self.outerWidth() / 2) + "px"
            });

            return self;
        },

        findHasNotAttr: function (query, attr) {
            var
                self = $(this);

            return self.find(query + ":not([" + attr + "])");
        },

        findByAttr: function (query, attr, value) {
            return $(this).find(query + "[" + attr + "=\"" + value + "\"]");
        },

        textsAsArray: function () {
            var
                arr = [];

            $(this).each(function (i, obj) {
                arr.push(obj.textContent.trim());
            });

            return arr;
        },

        findElementByTextContent: function (text) {
            return $(this).contents().filter(function () {
                // debugger
                return $(this).text().trim() === text.trim();
            });
        },

        sortByTextContent: function (order) {
            var
                self = $(this),
                arr = [],
                asArr = [];

            order = order.toLocaleLowerCase();
            asArr = $(this).textsAsArray();

            if (order === "asc") {
                asArr = asArr.asc();
            } else if (order === "desc") {
                asArr = asArr.desc();
            }

            asArr.map(function (text) {
                arr.push(self.parent().findElementByTextContent(text).get(0));
            });

            return $(arr);
        }

    });

}(jQuery));
<|endoftext|>


<commit_before>var
    esPhinx,
    Iterable;

(function($) {
    "use strict";

    $.prototype.extend({

        autocomplete: function(elementsList, options, callback) {
            var
                nonCharacterKeysPattern = new RegExp($
                    .nonCharacterKeysPattern()),
                inline = false,
                searchTextBox = this,

                resolveArguments = function(options) {
                    if (Object.getPrototypeOf(options) !=
                        Object.getPrototypeOf({})) {
                        options = {};
                    }

                    if (!options.caseSensitive ||
                        typeof options.caseSensitive != "boolean") {
                        options.caseSensitive = false;
                    }

                    if (!options.orderlySearch ||
                        typeof options.orderlySearch != "boolean") {
                        options.orderlySearch = false;
                    }

                    if (!options.afterAmountChar) {
                        options.afterAmountChar = 1;
                    }

                },

                resolveAmountChar = function(options) {
                    if (options.afterAmountChar) {
                        return options.afterAmountChar;
                    } else {
                        return 1;
                    }
                },

                inlineSearch = function(elements, options) {
                    var
                        eventListenerMethod,
                        resolvedList,
                        resolvedElementsParent,
                        foundElements,
                        i,
                        currentSearchText = searchTextBox.value(),
                        currentElements = elements.clone(true),
                        elements2Recovery = [];

                    if (typeof eventListenerMethod == "function") {
                        searchTextBox.off("input", eventListenerMethod);
                        searchTextBox.off("focus", eventListenerMethod);
                    }

                    eventListenerMethod = function(e) {
                        if (searchTextBox.value()) {
                            if (searchTextBox.value().length <
                                currentSearchText.length) {
                                i = searchTextBox.value().length +
                                    options.afterAmountChar - 2;
                                currentElements = elements2Recovery[i];
                            }

                            currentSearchText = searchTextBox.value();
                            resolvedList = resolveList(currentElements,
                                options.order);
                            foundElements = resolvedList.elements;

                            if (foundElements.some()) {
                                elements2Recovery.push(foundElements);
                                resolvedElementsParent = foundElements
                                    .parent().first().clone();
                                currentElements = resolvedElementsParent
                                    .append(foundElements);
                            }

                            callback.call($(this), foundElements, resolvedList
                                          .maps, e);
                        } else {
                            // a clone must be done because a change in one doesn't will be done on other
                            elements2Recovery = [];
                            currentElements = elements.clone(true);
                            callback.call($(this), $(), e);
                        }

                    };

                    searchTextBox.on("input", eventListenerMethod);
                    searchTextBox.on("focus", eventListenerMethod);
                },

                remoteSearch = function(options) {
                    var
                        word,
                        prevText,
                        nextText,
                        elements,
                        params,
                        resolvedList,
                        clipboardData,
                        amountChar = options.afterAmountChar,

                        request = function(e) {
                            if (options.remote.params) {
                                params = options.remote.params
                                    .call(searchTextBox);
                            }

                            $.Ajax.new(options.remote.url
                                .call(searchTextBox), {
                                params: params,
                                progress: function(xhr) {
                                    if (options.remote.loading) {
                                        options.remote
                                            .loading(xhr, e);
                                    }
                                },
                                complete: function() {
                                    $(e.target).off("keypress");
                                },
                                success: function(r) {
                                    elements = $(r);
                                    // debugger
                                    resolvedList = resolveList(
                                        elements, options.order);
                                    callback.call(searchTextBox,
                                        resolvedList.elements,
                                        resolvedList.maps, e);
                                    if (elements.length) {
                                        inlineSearch(elements, options);
                                    }
                                }
                            });
                        };

                    searchTextBox.on("keyup", function(e) {
                        word = searchTextBox.value();

                        if (word.length > amountChar) {
                            inline = true;
                        } else if (word.length < amountChar) {
                            inline = false;
                        }

                        if (word.length == amountChar &&
                            !nonCharacterKeysPattern.test(e.key) &&
                            !inline &&
                            options.remote) {
                            $(e.target).on("keypress", function(e) {
                                e.preventDefault();
                            });

                            request(e);
                        }

                    });

                    searchTextBox.on("paste", function(e) {
                        word = searchTextBox.value();
                        prevText = word
                            .slice(0, searchTextBox.cursorStart());
                        nextText = word
                            .slice(searchTextBox.cursorEnd(), word.length);

                        clipboardData = e.clipboardData.getData("text");

                        if (word.length >= amountChar) {
                            inline = true;
                        } else {
                            inline = false;
                        }

                        word = prevText + clipboardData + nextText;

                        if (word.length >= amountChar &&
                            !inline &&
                            options.remote) {
                            searchTextBox.value(word);

                            request(e);
                            e.preventDefault();
                        }
                    });

                },

                mapIf = function(searchTextAsArray, textNodeAsArray) {
                    var
                        iterator,
                        sliceIndex,
                        regexp,
                        textNodeHashSet = Object
                            .asCountableLiteralObject(textNodeAsArray),
                        mapped = {},
                        count = 0,
                        index = 0,
                        previousIndex = -1;

                    if (textNodeHashSet.length >= searchTextAsArray.length) {
                        iterator = Iterable.Proxy.new(searchTextAsArray);
                        iterator.each(function(slice) {
                            slice = slice.trim();
                            index = Object.firstFromASlice(textNodeHashSet,
                                                             slice, index,
                                                             options
                                                             .caseSensitive);

                            if (index && ((options.orderlySearch &&
                                index > previousIndex) ||
                                !options.orderlySearch) &&
                                textNodeAsArray.countSlice(slice,
                                    options.caseSensitive) >=
                                    searchTextAsArray.count(slice)) {

                                previousIndex = index;

                                if (options.caseSensitive) {
                                    regexp = new RegExp(slice);
                                } else {
                                    regexp = new RegExp(slice, "i");
                                }

                                sliceIndex = textNodeAsArray[index]
                                    .search(regexp);
                                mapped[index] = textNodeAsArray[index]
                                    .slice(sliceIndex, sliceIndex + slice
                                           .length);
                                Object.delete(textNodeHashSet, index);
                                count += 1;
                            } else {
                                this.finalize();
                            }
                        });

                        if (count == searchTextAsArray.length) {
                            return mapped;
                        }
                    }

                    return false;
                },

                // resolveList = function(list, order = "desc") {
                resolveList = function(list, order) {
                    if (order != "asc") {order = "desc";}
                    var
                        textNode,
                        mapped,
                        textNodeAsArray,
                        sorted,
                        found = {elements: $(), maps: []},
                        textBoxContentArr = searchTextBox.value().split(" ")
                            .compact();

                    if (list.elements().count() == 1 &&
                        list.instanceOf(window.Element)) {
                        list = list.childElements();
                    }

                    if (order == "asc") {
                        sorted = list.ascendingSortTextNode();
                    } else {
                        sorted = list.descendingSortTextNode();
                    }

                    sorted.each(function(element) {
                        textNode = $(element).textContent().trim();
                        textNodeAsArray = textNode.split(" ");
                        mapped = mapIf(textBoxContentArr, textNodeAsArray);
                        if (mapped) {
                            found.elements.push(element);
                            found.maps.push(mapped);
                        }
                    });

                    return found;
                };

            resolveArguments(options);

            // init
            if (Object.getPrototypeOf(elementsList) != Object.prototype) {
                inlineSearch(elementsList, options);
            } else {
                options = elementsList;
                remoteSearch(options);
            }
        }

    });

})(esPhinx);
<commit_msg>remove unecessary code<commit_after>var
    esPhinx,
    Iterable;

(function($) {
    "use strict";

    $.prototype.extend({

        autocomplete: function(elementsList, options, callback) {
            var
                nonCharacterKeysPattern = new RegExp($
                    .nonCharacterKeysPattern()),
                inline = false,
                searchTextBox = this,

                resolveArguments = function(options) {
                    if (Object.getPrototypeOf(options) !=
                        Object.getPrototypeOf({})) {
                        options = {};
                    }

                    if (!options.caseSensitive ||
                        typeof options.caseSensitive != "boolean") {
                        options.caseSensitive = false;
                    }

                    if (!options.orderlySearch ||
                        typeof options.orderlySearch != "boolean") {
                        options.orderlySearch = false;
                    }

                    if (!options.afterAmountChar ||
                        typeof options.afterAmountChar != "number") {
                        options.afterAmountChar = 1;
                    }

                },

                inlineSearch = function(elements, options) {
                    var
                        eventListenerMethod,
                        resolvedList,
                        resolvedElementsParent,
                        foundElements,
                        i,
                        currentSearchText = searchTextBox.value(),
                        currentElements = elements.clone(true),
                        elements2Recovery = [];

                    if (typeof eventListenerMethod == "function") {
                        searchTextBox.off("input", eventListenerMethod);
                        searchTextBox.off("focus", eventListenerMethod);
                    }

                    eventListenerMethod = function(e) {
                        if (searchTextBox.value()) {
                            if (searchTextBox.value().length <
                                currentSearchText.length) {
                                i = searchTextBox.value().length +
                                    options.afterAmountChar - 2;
                                currentElements = elements2Recovery[i];
                            }

                            currentSearchText = searchTextBox.value();
                            resolvedList = resolveList(currentElements,
                                options.order);
                            foundElements = resolvedList.elements;

                            if (foundElements.some()) {
                                elements2Recovery.push(foundElements);
                                resolvedElementsParent = foundElements
                                    .parent().first().clone();
                                currentElements = resolvedElementsParent
                                    .append(foundElements);
                            }

                            callback.call($(this), foundElements, resolvedList
                                          .maps, e);
                        } else {
                            // a clone must be done because a change in one doesn't will be done on other
                            elements2Recovery = [];
                            currentElements = elements.clone(true);
                            callback.call($(this), $(), e);
                        }

                    };

                    searchTextBox.on("input", eventListenerMethod);
                    searchTextBox.on("focus", eventListenerMethod);
                },

                remoteSearch = function(options) {
                    var
                        word,
                        prevText,
                        nextText,
                        elements,
                        params,
                        resolvedList,
                        clipboardData,
                        amountChar = options.afterAmountChar,

                        request = function(e) {
                            if (options.remote.params) {
                                params = options.remote.params
                                    .call(searchTextBox);
                            }

                            $.Ajax.new(options.remote.url
                                .call(searchTextBox), {
                                params: params,
                                progress: function(xhr) {
                                    if (options.remote.loading) {
                                        options.remote
                                            .loading(xhr, e);
                                    }
                                },
                                complete: function() {
                                    $(e.target).off("keypress");
                                },
                                success: function(r) {
                                    elements = $(r);
                                    // debugger
                                    resolvedList = resolveList(
                                        elements, options.order);
                                    callback.call(searchTextBox,
                                        resolvedList.elements,
                                        resolvedList.maps, e);
                                    if (elements.length) {
                                        inlineSearch(elements, options);
                                    }
                                }
                            });
                        };

                    searchTextBox.on("keyup", function(e) {
                        word = searchTextBox.value();

                        if (word.length > amountChar) {
                            inline = true;
                        } else if (word.length < amountChar) {
                            inline = false;
                        }

                        if (word.length == amountChar &&
                            !nonCharacterKeysPattern.test(e.key) &&
                            !inline &&
                            options.remote) {
                            $(e.target).on("keypress", function(e) {
                                e.preventDefault();
                            });

                            request(e);
                        }

                    });

                    searchTextBox.on("paste", function(e) {
                        word = searchTextBox.value();
                        prevText = word
                            .slice(0, searchTextBox.cursorStart());
                        nextText = word
                            .slice(searchTextBox.cursorEnd(), word.length);

                        clipboardData = e.clipboardData.getData("text");

                        if (word.length >= amountChar) {
                            inline = true;
                        } else {
                            inline = false;
                        }

                        word = prevText + clipboardData + nextText;

                        if (word.length >= amountChar &&
                            !inline &&
                            options.remote) {
                            searchTextBox.value(word);

                            request(e);
                            e.preventDefault();
                        }
                    });

                },

                mapIf = function(searchTextAsArray, textNodeAsArray) {
                    var
                        iterator,
                        sliceIndex,
                        regexp,
                        textNodeHashSet = Object
                            .asCountableLiteralObject(textNodeAsArray),
                        mapped = {},
                        count = 0,
                        index = 0,
                        previousIndex = -1;

                    if (textNodeHashSet.length >= searchTextAsArray.length) {
                        iterator = Iterable.Proxy.new(searchTextAsArray);
                        iterator.each(function(slice) {
                            slice = slice.trim();
                            index = Object.firstFromASlice(textNodeHashSet,
                                                             slice, index,
                                                             options
                                                             .caseSensitive);

                            if (index && ((options.orderlySearch &&
                                index > previousIndex) ||
                                !options.orderlySearch) &&
                                textNodeAsArray.countSlice(slice,
                                    options.caseSensitive) >=
                                    searchTextAsArray.count(slice)) {

                                previousIndex = index;

                                if (options.caseSensitive) {
                                    regexp = new RegExp(slice);
                                } else {
                                    regexp = new RegExp(slice, "i");
                                }

                                sliceIndex = textNodeAsArray[index]
                                    .search(regexp);
                                mapped[index] = textNodeAsArray[index]
                                    .slice(sliceIndex, sliceIndex + slice
                                           .length);
                                Object.delete(textNodeHashSet, index);
                                count += 1;
                            } else {
                                this.finalize();
                            }
                        });

                        if (count == searchTextAsArray.length) {
                            return mapped;
                        }
                    }

                    return false;
                },

                // resolveList = function(list, order = "desc") {
                resolveList = function(list, order) {
                    if (order != "asc") {order = "desc";}
                    var
                        textNode,
                        mapped,
                        textNodeAsArray,
                        sorted,
                        found = {elements: $(), maps: []},
                        textBoxContentArr = searchTextBox.value().split(" ")
                            .compact();

                    if (list.elements().count() == 1 &&
                        list.instanceOf(window.Element)) {
                        list = list.childElements();
                    }

                    if (order == "asc") {
                        sorted = list.ascendingSortTextNode();
                    } else {
                        sorted = list.descendingSortTextNode();
                    }

                    sorted.each(function(element) {
                        textNode = $(element).textContent().trim();
                        textNodeAsArray = textNode.split(" ");
                        mapped = mapIf(textBoxContentArr, textNodeAsArray);
                        if (mapped) {
                            found.elements.push(element);
                            found.maps.push(mapped);
                        }
                    });

                    return found;
                };

            resolveArguments(options);

            // init
            if (Object.getPrototypeOf(elementsList) != Object.prototype) {
                inlineSearch(elementsList, options);
            } else {
                options = elementsList;
                remoteSearch(options);
            }
        }

    });

})(esPhinx);
<|endoftext|>


<commit_before>/**
 * Returns a template function (function that accepts template parameters and returns a string)
 * to generate an aggregation query.
 * Valid options to define the query template are:
 * - placement
 * The query template parameters taken by the result template function are:
 * - sourceQuery
 * - res
 * - columns
 */
const templateForOptions = (options) => {
    let templateFn = aggregationQueryTemplates[options.placement];
    if (!templateFn) {
        throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
    }
    return templateFn;
};

/**
 * Generates an aggregation query given the aggregation options:
 * - query
 * - resolution
 * - columns
 * - placement
 */
const queryForOptions = (options) => templateForOptions(options)({
    sourceQuery: options.query,
    res: options.resolution,
    columns: options.columns
});

module.exports = queryForOptions;

const SUPPORTED_AGGREGATE_FUNCTIONS = {
    'count': {
        sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
    },
    'avg': {
        sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
    },
    'sum': {
        sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
    },
    'min': {
        sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
    },
    'max': {
        sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
    }
};

const aggregateColumns = ctx => {
    // TODO: always add count
    let columns = ctx.columns || {};
    if (Object.keys(columns).length === 0) {
        // default aggregation
        columns = {
            _cdb_feature_count: {
                aggregate_function: 'count'
            }
        };
    }
    return Object.keys(columns).map(column_name => {
        const aggregate_function = columns[column_name].aggregate_function || 'count';
        const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
        if (!aggregate_definition) {
            throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
        }
        const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
        return `${aggregate_expression} AS ${column_name}`;
    }).join(', ');
};

// Notes:
// * ${ctx.res*0.00028/256}*!scale_denominator! is equivalent to
//   ${ctx.res/256}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))
// * We need to filter spatially using !bbox! to make the queries efficient because
//   the filter added by Mapnik (wrapping the query)
//   is only applied after the aggregation.
// * This queries are used for rendering and the_geom is omitted in the results for better performance

const aggregationQueryTemplates = {
    'centroid': ctx => `
        WITH _cdb_params AS (
            SELECT
            (${ctx.res*0.00028/256}*!scale_denominator!)::double precision AS res,
            !bbox! AS bbox
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(
                ST_MakePoint(
                    AVG(ST_X(_cdb_query.the_geom_webmercator)),
                    AVG(ST_Y(_cdb_query.the_geom_webmercator))
                ), 3857
            ) AS the_geom_webmercator,
            ${aggregateColumns(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
    `,

    'point-grid': ctx => `
    WITH _cdb_params AS (
        SELECT
        (${ctx.res*0.00028/256}*!scale_denominator!)::double precision AS res,
        !bbox! AS bbox
    ),
    _cdb_clusters AS (
        SELECT
        Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
        Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy,
        ${aggregateColumns(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE the_geom_webmercator && _cdb_params.bbox
        GROUP BY _cdb_gx, _cdb_gy
    )
    SELECT
        ST_SetSRID(ST_MakePoint(_cdb_gx*(res+0.5), _cdb_gy*(res+0.5)), 3857) AS the_geom_webmercator,
        _cdb_feature_count
        FROM _cdb_clusters, _cdb_params
    `,

    'point-sample': ctx => `
        WITH _cdb_params AS (
            SELECT

            (${ctx.res*0.00028/256}*!scale_denominator!)::double precision AS res,
            !bbox! AS bbox
        ), _cdb_clusters AS (
            SELECT
                MIN(cartodb_id) AS cartodb_id,
                ${aggregateColumns(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
            GROUP BY
                Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
                Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
        ) SELECT
            _cdb_clusters.cartodb_id,
            the_geom, the_geom_webmercator,
            _cdb_feature_count
        FROM
            _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
            ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
    `
};
<commit_msg>Fix aggregation resolution parameter<commit_after>/**
 * Returns a template function (function that accepts template parameters and returns a string)
 * to generate an aggregation query.
 * Valid options to define the query template are:
 * - placement
 * The query template parameters taken by the result template function are:
 * - sourceQuery
 * - res
 * - columns
 */
const templateForOptions = (options) => {
    let templateFn = aggregationQueryTemplates[options.placement];
    if (!templateFn) {
        throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
    }
    return templateFn;
};

/**
 * Generates an aggregation query given the aggregation options:
 * - query
 * - resolution
 * - columns
 * - placement
 */
const queryForOptions = (options) => templateForOptions(options)({
    sourceQuery: options.query,
    res: options.resolution,
    columns: options.columns
});

module.exports = queryForOptions;

const SUPPORTED_AGGREGATE_FUNCTIONS = {
    'count': {
        sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
    },
    'avg': {
        sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
    },
    'sum': {
        sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
    },
    'min': {
        sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
    },
    'max': {
        sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
    }
};

const aggregateColumns = ctx => {
    // TODO: always add count
    let columns = ctx.columns || {};
    if (Object.keys(columns).length === 0) {
        // default aggregation
        columns = {
            _cdb_feature_count: {
                aggregate_function: 'count'
            }
        };
    }
    return Object.keys(columns).map(column_name => {
        const aggregate_function = columns[column_name].aggregate_function || 'count';
        const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
        if (!aggregate_definition) {
            throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
        }
        const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
        return `${aggregate_expression} AS ${column_name}`;
    }).join(', ');
};

// Notes:
// * ${256*0.00028/ctx.res}*!scale_denominator! is equivalent to
//   ${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))
// * We need to filter spatially using !bbox! to make the queries efficient because
//   the filter added by Mapnik (wrapping the query)
//   is only applied after the aggregation.
// * This queries are used for rendering and the_geom is omitted in the results for better performance

const aggregationQueryTemplates = {
    'centroid': ctx => `
        WITH _cdb_params AS (
            SELECT
            (${256*0.00028/ctx.res}*!scale_denominator!)::double precision AS res,
            !bbox! AS bbox
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(
                ST_MakePoint(
                    AVG(ST_X(_cdb_query.the_geom_webmercator)),
                    AVG(ST_Y(_cdb_query.the_geom_webmercator))
                ), 3857
            ) AS the_geom_webmercator,
            ${aggregateColumns(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
    `,

    'point-grid': ctx => `
    WITH _cdb_params AS (
        SELECT
        (${256*0.00028/ctx.res}*!scale_denominator!)::double precision AS res,
        !bbox! AS bbox
    ),
    _cdb_clusters AS (
        SELECT
        Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
        Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy,
        ${aggregateColumns(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE the_geom_webmercator && _cdb_params.bbox
        GROUP BY _cdb_gx, _cdb_gy
    )
    SELECT
        ST_SetSRID(ST_MakePoint(_cdb_gx*(res+0.5), _cdb_gy*(res+0.5)), 3857) AS the_geom_webmercator,
        _cdb_feature_count
        FROM _cdb_clusters, _cdb_params
    `,

    'point-sample': ctx => `
        WITH _cdb_params AS (
            SELECT

            (${256*0.00028/ctx.res}*!scale_denominator!)::double precision AS res,
            !bbox! AS bbox
        ), _cdb_clusters AS (
            SELECT
                MIN(cartodb_id) AS cartodb_id,
                ${aggregateColumns(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
            GROUP BY
                Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
                Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
        ) SELECT
            _cdb_clusters.cartodb_id,
            the_geom, the_geom_webmercator,
            _cdb_feature_count
        FROM
            _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
            ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
    `
};
<|endoftext|>


<commit_before>/* Navia Engine Base Class */
console.log("Test");
class NaviaBase {

    constructor(){
        super();
        this.modules = [];
    }

    //Navia Starts here
    start() {

    }

    //Called once each frame
    update(obj){

    }

}<commit_msg>Fixed typo<commit_after>/* Navia Engine Base Class */

class NaviaBase {

    constructor(){
        this.modules = [];
    }

    //Navia Starts here
    start() {

    }

    //Called once each frame
    update(obj){

    }

}<|endoftext|>