<commit_before>/*
 *
 * (c) Copyright Ascensio System SIA 2010-2020
 *
 * This program is a free software product. You can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License (AGPL)
 * version 3 as published by the Free Software Foundation. In accordance with
 * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
 * that Ascensio System SIA expressly excludes the warranty of non-infringement
 * of any third-party rights.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE. For
 * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
 *
 * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha
 * street, Riga, Latvia, EU, LV-1050.
 *
 * The  interactive user interfaces in modified source and object code versions
 * of the Program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU AGPL version 3.
 *
 * Pursuant to Section 7(b) of the License you must retain the original Product
 * logo when distributing the program. Pursuant to Section 7(e) we decline to
 * grant you any rights under trademark law for use of our trademarks.
 *
 * All the Product's GUI elements, including illustrations and icon sets, as
 * well as technical writing content are licensed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International. See the License
 * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
 *
 */

/**
 *  AutoCorrectDialog.js
 *
 *  Created by Julia Radzhabova on 03.07.2020
 *  Copyright (c) 2020 Ascensio System SIA. All rights reserved.
 *
 */
if (Common === undefined)
    var Common = {};
define([
    'common/main/lib/component/ListView',
    'common/main/lib/component/Window'
], function () { 'use strict';

    Common.Views.AutoCorrectDialog = Common.UI.Window.extend(_.extend({
        options: {
            width: 448,
            cls: 'modal-dlg',
            buttons: null
        },

        initialize : function(options) {
            var filter = Common.localStorage.getKeysFilter();
            this.appPrefix = (filter && filter.length) ? filter.split(',')[0] : '';

            _.extend(this.options, {
                title: this.textTitle
            }, options || {});

            this.template = [
                '
',
                    '
',
                        '
',
                            '
',
                                '',
                                    '' + this.textMathCorrect + '',
                                '	',
                            '
',
                            '
',
                                '',
                                    '' + this.textReplace + '',
                                    '' + this.textBy + '',
                                '	',
                            '
',
                            '
',
                                '',
                                    '

',
                                    '

',
                                '	',
                            '
',
                            '
',
                                '',
                                    '

',
                                '	',
                            '
',
                            '
',
                                '',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-reset" style="min-width: 100px;">' + this.textReset + '</button>',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-delete" style="min-width: 100px;float: right;">' + this.textDelete + '</button>',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-edit" style="min-width: 100px;float: right;margin-right:5px;">' + this.textAdd+ '</button>',
                                '	',
                            '
',
                        '
',
                    '
',
                '
',
                '

',
                '
',
                    '<button class="btn normal dlg-btn" result="cancel" style="width: 86px;">' + this.closeButtonText + '</button>',
                '
'
            ].join('');

            this.options.tpl = _.template(this.template)(this.options);
            this.mathStore = this.options.mathStore || [];
            this.api = this.options.api;

            var path = this.appPrefix + "settings-math-correct";
            var value = Common.Utils.InternalSettings.get(path + "-add");
            this.arrAdd = value ? JSON.parse(value) : {};
            value = Common.Utils.InternalSettings.get(path + "-rem");
            this.arrRem = value ? JSON.parse(value) : [];

            Common.UI.Window.prototype.initialize.call(this, this.options);
        },

        render: function() {
            Common.UI.Window.prototype.render.call(this);

            var $window = this.getChild();
            var me = this;

            this.onResetList();

            // special
            this.mathList = new Common.UI.ListView({
                el: $window.find('#auto-correct-math-list'),
                store: this.mathStore,
                simpleAddMode: true,
                template: _.template(['

'].join('')),
                itemTemplate: _.template([
                    '<div id="<%= id %>" class="list-item" style="pointer-events:none;width: 100%;display:flex;">',
                        '<div style="min-width:110px;padding-right: 5px;<% if (defaultDisabled) { %> font-style:italic; opacity: 0.5;<% } %>"><%= replaced %>
',
                        '<div style="flex-grow:1;font-family: Cambria Math;font-size:13px;<% if (defaultDisabled) { %> font-style:italic; opacity: 0.5;<% } %>"><%= by %>
',
                    '
'
                ].join('')),
                scrollAlwaysVisible: true
            });
            this.mathList.on('item:select', _.bind(this.onSelectMathItem, this));

            this.inputReplace = new Common.UI.InputField({
                el               : $window.find('#auto-correct-replace'),
                allowBlank       : true,
                validateOnChange : true,
                validation       : function () { return true; }
            }).on ('changing', function (input, value) {
                var _selectedItem;
                if (value.length) {
                    var store = me.mathList.store;
                    _selectedItem = store.find(function(item) {
                        if ( item.get('replaced').indexOf(value) == 0) {
                            return true;
                        }
                    });
                    if (_selectedItem) {
                        me.mathList.selectRecord(_selectedItem, true);
                        me.mathList.scrollToRecord(_selectedItem);
                    } else {
                        me.mathList.deselectAll();
                    }
                } else {
                    me.mathList.deselectAll();
                }
                me.updateControls(_selectedItem);
            });

            this.inputReplace.cmpEl.find('input').on('keydown', function(event){
                if (event.key == 'ArrowDown') {
                    var _selectedItem = me.mathList.getSelectedRec() || me.mathList.store.at(0);
                    if (_selectedItem) {
                        me.mathList.selectRecord(_selectedItem);
                        me.mathList.scrollToRecord(_selectedItem);
                    }
                    _.delay(function(){
                        me.mathList.cmpEl.find('.listview').focus();
                    },10);

                }
            });

            this.inputBy = new Common.UI.InputField({
                el               : $window.find('#auto-correct-by'),
                allowBlank       : true,
                validateOnChange : true,
                validation       : function () { return true; }
            }).on ('changing', function (input, value) {
                me.updateControls();
            });
            // this.inputBy.cmpEl.find('input').css('font-size', '13px');

            this.btnReset = new Common.UI.Button({
                el: $('#auto-correct-btn-reset')
            });
            this.btnReset.on('click', _.bind(this.onResetList, this, true));

            this.btnEdit = new Common.UI.Button({
                el: $('#auto-correct-btn-edit')
            });
            this.btnEdit.on('click', _.bind(this.onEdit, this, false));

            this.btnDelete = new Common.UI.Button({
                el: $('#auto-correct-btn-delete')
            });
            this.btnDelete.on('click', _.bind(this.onDelete, this, false));

            this.updateControls();

            $window.find('.dlg-btn').on('click', _.bind(this.onBtnClick, this));
        },

        onSelectMathItem: function(lisvView, itemView, record) {
            if (record) {
                this.inputReplace.setValue(record.get('replaced'));
                this.inputBy.setValue(record.get('by'));
            }
            this.updateControls(record);
        },

        updateControls: function(rec) {
            if (!this.mathList) return;

            rec = rec || this.mathList.getSelectedRec();
            if (rec) {
                this.btnDelete.setCaption(rec.get('defaultDisabled') ? this.textRestore : this.textDelete);
                this.btnEdit.setDisabled(!this.inputBy.getValue() || this.inputBy.getValue() === rec.get('by'));
            } else {
                this.btnDelete.setCaption(this.textDelete);
                this.btnDelete.setCaption(this.textDelete);
            }
            this.btnDelete.setDisabled(!rec);
            this.btnEdit.setCaption(rec ? this.textEdit : this.textAdd);
        },

        show: function() {
            Common.UI.Window.prototype.show.apply(this, arguments);

            var me = this;
            _.delay(function(){
                $('input', me.inputReplace.cmpEl).select().focus();
            },100);
        },

        onBtnClick: function(event) {
            this.close();
        },

        onPrimary: function(event) {
            return true;
        },

        onDelete: function() {
            var rec = this.mathList.getSelectedRec();
            if (rec) {
                if (rec.get('defaultValue')) {
                    var path = this.appPrefix + "settings-math-correct-rem";
                    var disabled = !rec.get('defaultDisabled');
                    rec.set('defaultDisabled', disabled);
                    if (disabled)
                        this.arrRem.push(rec.get('replaced'));
                    else
                        this.arrRem.splice(this.arrRem.indexOf(rec.get('replaced')), 1);
                    var val = JSON.stringify(this.arrRem);
                    Common.Utils.InternalSettings.set(path, val);
                    Common.localStorage.setItem(path, val);
                    this.btnDelete.setCaption(disabled ? this.textRestore : this.textDelete);
                } else {
                    this.mathStore.remove(rec);
                }
                this.api.asc_deleteFromAutoCorrectMathSymbols(rec.get('replaced'));
            }
        },

        onEdit: function() {
            var rec = this.mathList.getSelectedRec();
            if (rec) {
                rec.set('by', this.inputBy.getValue());
            } else {
                rec = this.mathStore.add({
                    replaced: this.inputReplace.getValue(),
                    by: this.inputBy.getValue(),
                    defaultDisabled: false
                });
                this.mathList.selectRecord(rec);
                this.mathList.scrollToRecord(rec);
            }
            this.arrAdd[rec.get('replaced')] = rec.get('by');
            var path = this.appPrefix + "settings-math-correct-add";
            var val = JSON.stringify(this.arrAdd);
            Common.Utils.InternalSettings.set(path, val);
            Common.localStorage.setItem(path, val);
            this.api.asc_AddOrEditFromAutoCorrectMathSymbols(rec.get('replaced'), rec.get('by'));
        },

        onResetList: function(clear) {
            if (this.mathStore.length>0 && !clear) return;

            if (clear) {
                // remove storage data
                var path = this.appPrefix + "settings-math-correct";
                var val = JSON.stringify([]);
                Common.Utils.InternalSettings.set(path + "-add", val);
                Common.localStorage.setItem(path + "-add", val);
                Common.Utils.InternalSettings.set(path + "-rem", val);
                Common.localStorage.setItem(path + "-rem", val);
                this.arrAdd = {};
                this.arrRem = [];
            }
            var arrAdd = this.arrAdd,
                arrRem = this.arrRem;

            var arr = (this.api) ? this.api.asc_getAutoCorrectMathSymbols() : [],
                data = [];
            _.each(arr, function(item, index){
                var itm = {
                    replaced: item[0],
                    defaultDisabled: arrRem.indexOf(item[0])>-1
                };
                var by = arrAdd[item[0]];
                if (typeof item[1]=='object') {
                    itm.defaultValue = '';
                    _.each(item[1], function(ch){
                        itm.defaultValue += Common.Utils.String.encodeSurrogateChar(ch);
                    });
                    itm.by = by ? by : itm.defaultValue;
                } else {
                    itm.defaultValue = Common.Utils.String.encodeSurrogateChar(item[1]);
                    itm.by = by ? by : itm.defaultValue;
                }
                data.push(itm);
                by && (arrAdd[item[0]] = undefined);
            });
            for(var key in arrAdd){
                if(arrAdd.hasOwnProperty(key) && arrAdd[key]){
                    var itm = {
                        replaced: key,
                        by: arrAdd[key],
                        defaultDisabled: false
                    };
                    data.push(itm);
                }
            }
            this.mathStore.reset(data);
            this.updateControls();
        },

        textTitle: 'AutoCorrect',
        textMathCorrect: 'Math AutoCorrect',
        textReplace: 'Replace:',
        textBy: 'By:',
        textReset: 'Reset to default',
        textAdd: 'Add',
        textEdit: 'Replace',
        textDelete: 'Delete',
        textRestore: 'Restore'

    }, Common.Views.AutoCorrectDialog || {}))
});
<commit_msg>[DE] Fix changing autocorrect list<commit_after>/*
 *
 * (c) Copyright Ascensio System SIA 2010-2020
 *
 * This program is a free software product. You can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License (AGPL)
 * version 3 as published by the Free Software Foundation. In accordance with
 * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
 * that Ascensio System SIA expressly excludes the warranty of non-infringement
 * of any third-party rights.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE. For
 * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
 *
 * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha
 * street, Riga, Latvia, EU, LV-1050.
 *
 * The  interactive user interfaces in modified source and object code versions
 * of the Program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU AGPL version 3.
 *
 * Pursuant to Section 7(b) of the License you must retain the original Product
 * logo when distributing the program. Pursuant to Section 7(e) we decline to
 * grant you any rights under trademark law for use of our trademarks.
 *
 * All the Product's GUI elements, including illustrations and icon sets, as
 * well as technical writing content are licensed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International. See the License
 * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
 *
 */

/**
 *  AutoCorrectDialog.js
 *
 *  Created by Julia Radzhabova on 03.07.2020
 *  Copyright (c) 2020 Ascensio System SIA. All rights reserved.
 *
 */
if (Common === undefined)
    var Common = {};
define([
    'common/main/lib/component/ListView',
    'common/main/lib/component/Window'
], function () { 'use strict';

    Common.Views.AutoCorrectDialog = Common.UI.Window.extend(_.extend({
        options: {
            width: 448,
            cls: 'modal-dlg',
            buttons: null
        },

        initialize : function(options) {
            var filter = Common.localStorage.getKeysFilter();
            this.appPrefix = (filter && filter.length) ? filter.split(',')[0] : '';

            _.extend(this.options, {
                title: this.textTitle
            }, options || {});

            this.template = [
                '
',
                    '
',
                        '
',
                            '
',
                                '',
                                    '' + this.textMathCorrect + '',
                                '	',
                            '
',
                            '
',
                                '',
                                    '' + this.textReplace + '',
                                    '' + this.textBy + '',
                                '	',
                            '
',
                            '
',
                                '',
                                    '

',
                                    '

',
                                '	',
                            '
',
                            '
',
                                '',
                                    '

',
                                '	',
                            '
',
                            '
',
                                '',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-reset" style="min-width: 100px;">' + this.textReset + '</button>',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-delete" style="min-width: 100px;float: right;">' + this.textDelete + '</button>',
                                    '<button type="button" class="btn btn-text-default auto" id="auto-correct-btn-edit" style="min-width: 100px;float: right;margin-right:5px;">' + this.textAdd+ '</button>',
                                '	',
                            '
',
                        '
',
                    '
',
                '
',
                '

',
                '
',
                    '<button class="btn normal dlg-btn" result="cancel" style="width: 86px;">' + this.closeButtonText + '</button>',
                '
'
            ].join('');

            this.options.tpl = _.template(this.template)(this.options);
            this.mathStore = this.options.mathStore || [];
            this.api = this.options.api;

            var path = this.appPrefix + "settings-math-correct";
            var value = Common.Utils.InternalSettings.get(path + "-add");
            this.arrAdd = value ? JSON.parse(value) : {};
            value = Common.Utils.InternalSettings.get(path + "-rem");
            this.arrRem = value ? JSON.parse(value) : [];

            Common.UI.Window.prototype.initialize.call(this, this.options);
        },

        render: function() {
            Common.UI.Window.prototype.render.call(this);

            var $window = this.getChild();
            var me = this;

            this.onResetList();

            // special
            this.mathList = new Common.UI.ListView({
                el: $window.find('#auto-correct-math-list'),
                store: this.mathStore,
                simpleAddMode: true,
                template: _.template(['

'].join('')),
                itemTemplate: _.template([
                    '<div id="<%= id %>" class="list-item" style="pointer-events:none;width: 100%;display:flex;">',
                        '<div style="min-width:110px;padding-right: 5px;<% if (defaultDisabled) { %> font-style:italic; opacity: 0.5;<% } %>"><%= replaced %>
',
                        '<div style="flex-grow:1;font-family: Cambria Math;font-size:13px;<% if (defaultDisabled) { %> font-style:italic; opacity: 0.5;<% } %>"><%= by %>
',
                    '
'
                ].join('')),
                scrollAlwaysVisible: true
            });
            this.mathList.on('item:select', _.bind(this.onSelectMathItem, this));

            this.inputReplace = new Common.UI.InputField({
                el               : $window.find('#auto-correct-replace'),
                allowBlank       : true,
                validateOnChange : true,
                validation       : function () { return true; }
            }).on ('changing', function (input, value) {
                var _selectedItem;
                if (value.length) {
                    var store = me.mathList.store;
                    _selectedItem = store.find(function(item) {
                        if ( item.get('replaced').indexOf(value) == 0) {
                            return true;
                        }
                    });
                    if (_selectedItem) {
                        me.mathList.scrollToRecord(_selectedItem, true);
                        if (_selectedItem.get('replaced') == value)
                            me.mathList.selectRecord(_selectedItem, true);
                        else
                            _selectedItem = null;
                    }
                }
                (!_selectedItem) && me.mathList.deselectAll();
                me.updateControls(_selectedItem);
            });

            this.inputReplace.cmpEl.find('input').on('keydown', function(event){
                if (event.key == 'ArrowDown') {
                    var _selectedItem = me.mathList.getSelectedRec() || me.mathList.store.at(0);
                    if (_selectedItem) {
                        me.mathList.selectRecord(_selectedItem);
                        me.mathList.scrollToRecord(_selectedItem);
                    }
                    _.delay(function(){
                        me.mathList.cmpEl.find('.listview').focus();
                    },10);

                }
            });

            this.inputBy = new Common.UI.InputField({
                el               : $window.find('#auto-correct-by'),
                allowBlank       : true,
                validateOnChange : true,
                validation       : function () { return true; }
            }).on ('changing', function (input, value) {
                me.updateControls();
            });
            // this.inputBy.cmpEl.find('input').css('font-size', '13px');

            this.btnReset = new Common.UI.Button({
                el: $('#auto-correct-btn-reset')
            });
            this.btnReset.on('click', _.bind(this.onResetList, this, true));

            this.btnEdit = new Common.UI.Button({
                el: $('#auto-correct-btn-edit')
            });
            this.btnEdit.on('click', _.bind(this.onEdit, this, false));

            this.btnDelete = new Common.UI.Button({
                el: $('#auto-correct-btn-delete')
            });
            this.btnDelete.on('click', _.bind(this.onDelete, this, false));

            this.updateControls();

            $window.find('.dlg-btn').on('click', _.bind(this.onBtnClick, this));
        },

        onSelectMathItem: function(lisvView, itemView, record) {
            if (record) {
                this.inputReplace.setValue(record.get('replaced'));
                this.inputBy.setValue(record.get('by'));
            }
            this.updateControls(record);
        },

        updateControls: function(rec) {
            if (!this.mathList) return;

            rec = rec || this.mathList.getSelectedRec();
            if (rec) {
                this.btnDelete.setCaption(rec.get('defaultDisabled') ? this.textRestore : this.textDelete);
                this.btnEdit.setDisabled(!this.inputBy.getValue() || this.inputBy.getValue() === rec.get('by'));
            } else {
                this.btnDelete.setCaption(this.textDelete);
                this.btnEdit.setDisabled(!this.inputBy.getValue());
            }
            this.btnDelete.setDisabled(!rec);
            this.btnEdit.setCaption(rec ? this.textEdit : this.textAdd);
        },

        show: function() {
            Common.UI.Window.prototype.show.apply(this, arguments);

            var me = this;
            _.delay(function(){
                $('input', me.inputReplace.cmpEl).select().focus();
            },100);
        },

        onBtnClick: function(event) {
            this.close();
        },

        onPrimary: function(event) {
            return true;
        },

        onDelete: function() {
            var rec = this.mathList.getSelectedRec();
            if (rec) {
                if (rec.get('defaultValue')) {
                    var path = this.appPrefix + "settings-math-correct-rem";
                    var disabled = !rec.get('defaultDisabled');
                    rec.set('defaultDisabled', disabled);
                    if (disabled)
                        this.arrRem.push(rec.get('replaced'));
                    else
                        this.arrRem.splice(this.arrRem.indexOf(rec.get('replaced')), 1);
                    var val = JSON.stringify(this.arrRem);
                    Common.Utils.InternalSettings.set(path, val);
                    Common.localStorage.setItem(path, val);
                    this.btnDelete.setCaption(disabled ? this.textRestore : this.textDelete);
                } else {
                    this.mathStore.remove(rec);
                }
                this.api.asc_deleteFromAutoCorrectMathSymbols(rec.get('replaced'));
            }
        },

        onEdit: function() {
            var rec = this.mathList.getSelectedRec();
            if (rec) {
                rec.set('by', this.inputBy.getValue());
            } else {
                rec = this.mathStore.add({
                    replaced: this.inputReplace.getValue(),
                    by: this.inputBy.getValue(),
                    defaultDisabled: false
                });
                this.mathList.selectRecord(rec);
                this.mathList.scrollToRecord(rec);
            }
            this.arrAdd[rec.get('replaced')] = rec.get('by');
            var path = this.appPrefix + "settings-math-correct-add";
            var val = JSON.stringify(this.arrAdd);
            Common.Utils.InternalSettings.set(path, val);
            Common.localStorage.setItem(path, val);
            this.api.asc_AddOrEditFromAutoCorrectMathSymbols(rec.get('replaced'), rec.get('by'));
        },

        onResetList: function(clear) {
            if (this.mathStore.length>0 && !clear) return;

            if (clear) {
                // remove storage data
                var path = this.appPrefix + "settings-math-correct";
                var val = JSON.stringify([]);
                Common.Utils.InternalSettings.set(path + "-add", val);
                Common.localStorage.setItem(path + "-add", val);
                Common.Utils.InternalSettings.set(path + "-rem", val);
                Common.localStorage.setItem(path + "-rem", val);
                this.arrAdd = {};
                this.arrRem = [];
            }
            var arrAdd = this.arrAdd,
                arrRem = this.arrRem;

            var arr = (this.api) ? this.api.asc_getAutoCorrectMathSymbols() : [],
                data = [];
            _.each(arr, function(item, index){
                var itm = {
                    replaced: item[0],
                    defaultDisabled: arrRem.indexOf(item[0])>-1
                };
                var by = arrAdd[item[0]];
                if (typeof item[1]=='object') {
                    itm.defaultValue = '';
                    _.each(item[1], function(ch){
                        itm.defaultValue += Common.Utils.String.encodeSurrogateChar(ch);
                    });
                    itm.by = by ? by : itm.defaultValue;
                } else {
                    itm.defaultValue = Common.Utils.String.encodeSurrogateChar(item[1]);
                    itm.by = by ? by : itm.defaultValue;
                }
                data.push(itm);
                by && (arrAdd[item[0]] = undefined);
            });
            for(var key in arrAdd){
                if(arrAdd.hasOwnProperty(key) && arrAdd[key]){
                    var itm = {
                        replaced: key,
                        by: arrAdd[key],
                        defaultDisabled: false
                    };
                    data.push(itm);
                }
            }
            this.mathStore.reset(data);
            this.updateControls();
        },

        textTitle: 'AutoCorrect',
        textMathCorrect: 'Math AutoCorrect',
        textReplace: 'Replace:',
        textBy: 'By:',
        textReset: 'Reset to default',
        textAdd: 'Add',
        textEdit: 'Replace',
        textDelete: 'Delete',
        textRestore: 'Restore'

    }, Common.Views.AutoCorrectDialog || {}))
});
<|endoftext|>


<commit_before>import { build, Platform, createTargets } from 'electron-builder';
import { installOrRebuild } from 'electron-builder-lib/out/util/yarn';
import { createLazyProductionDeps } from 'electron-builder-lib/out/util/packageDependencies';
import shell from 'shelljs';
import path from 'path';
import fs from 'fs';
import rimraf from 'rimraf';
import spawn from 'cross-spawn';
import Log from './log';

/**
 * Promisfied rimraf.
 *
 * @param {string} dirPath - path to the dir to be deleted
 * @param {number} delay - delay the task by ms
 * @returns {Promise<any>}
 */
function removeDir(dirPath, delay = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            rimraf(dirPath, {
                maxBusyTries: 100
            }, (err) => {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            });
        }, delay);
    });
}

/**
 * Wrapper for electron-builder.
 */
export default class InstallerBuilder {
    /**
     * @param {MeteorDesktop} $ - context
     *
     * @constructor
     */
    constructor($) {
        this.log = new Log('electronBuilder');
        this.$ = $;
        this.firstPass = true;
        this.lastRebuild = {};
        this.currentContext = null;
    }

    /**
     * Prepares the last rebuild object for electron-builder.
     *
     * @param {string} arch
     * @param {string} platform
     * @returns {Object}
     */
    prepareLastRebuildObject(arch, platform = process.platform) {
        const productionDeps = createLazyProductionDeps(this.$.env.paths.electronApp.root);
        this.lastRebuild = {
            frameworkInfo: { version: this.$.getElectronVersion(), useCustomDist: true },
            platform,
            arch,
            productionDeps
        };
        return this.lastRebuild;
    }

    /**
     * Calls npm rebuild from electron-builder.
     * @param {string} arch
     * @param {string} platform
     * @param {boolean} install
     * @returns {Promise}
     */
    async installOrRebuild(arch, platform = process.platform, install = false) {
        this.log.debug(`calling installOrRebuild from electron-builder for arch ${arch}`);
        this.prepareLastRebuildObject(arch, platform);
        await installOrRebuild(this.$.desktop.getSettings().builderOptions || {},
            this.$.env.paths.electronApp.root, this.lastRebuild, install);
    }

    /**
     * Callback invoked before build is made. Ensures that app.asar have the right rebuilt
     * node_modules.
     *
     * @param {Object} context
     * @returns {Promise}
     */
    beforeBuild(context) {
        this.currentContext = Object.assign({}, context);
        return new Promise((resolve, reject) => {
            const platformMatches = process.platform === context.platform.nodeName;
            const rebuild = platformMatches && context.arch !== this.lastRebuild.arch;
            if (!platformMatches) {
                this.log.warn('skipping dependencies rebuild because platform is different, if you have native ' +
                    'node modules as your app dependencies you should od the build on the target platform only');
            }

            if (!rebuild) {
                this.moveNodeModulesOut()
                    .catch(e => reject(e))
                    .then(() => setTimeout(() => resolve(false), 2000));
                // Timeout helps on Windows to clear the file locks.
            } else {
                // Lets rebuild the node_modules for different arch.
                this.installOrRebuild(context.arch, context.platform.nodeName)
                    .catch(e => reject(e))
                    .then(() => this.$.electronApp.installLocalNodeModules(context.arch))
                    .catch(e => reject(e))
                    .then(() => {
                        this.$.electronApp.scaffold.createAppRoot();
                        this.$.electronApp.scaffold.copySkeletonApp();
                        return this.$.electronApp.packSkeletonToAsar(
                            [
                                this.$.env.paths.electronApp.meteorAsar,
                                this.$.env.paths.electronApp.desktopAsar,
                                this.$.env.paths.electronApp.extracted
                            ]
                        );
                    })
                    .catch(e => reject(e))
                    .then(() => this.moveNodeModulesOut())
                    .catch(e => reject(e))
                    .then(() => resolve(false));
            }
        });
    }

    /**
     * Callback to be invoked after packing. Restores node_modules to the .desktop-build.
     * @returns {Promise}
     */
    afterPack() {
        return new Promise((resolve, reject) => {
            this.log.debug('moving node_modules back');
            // Move node_modules back.

            shell.config.fatal = true;
            try {
                shell.mv(
                    this.$.env.paths.electronApp.tmpNodeModules,
                    this.$.env.paths.electronApp.nodeModules
                );
                resolve();
            } catch (e) {
                reject(e);
                return;
            } finally {
                shell.config.reset();
            }

            if (this.firstPass) {
                this.firstPass = false;
            }
            this.log.debug('node_modules moved back');

            this.wait()
                .catch(e => reject(e))
                .then(() => resolve());
        });
    }

    /**
     * This command kills orphaned MSBuild.exe processes.
     * Sometime after native node_modules compilation they are still writing some logs,
     * prevent node_modules from being deleted.
     */
    killMSBuild() {
        if (this.currentContext.platform.nodeName !== 'win32') {
            return;
        }
        try {
            const out = spawn
                .sync(
                    'wmic',
                    ['process', 'where', 'caption="MSBuild.exe"', 'get', 'processid']
                )
                .stdout.toString('utf-8')
                .split('\n');

            const regex = new RegExp(/(\d+)/, 'gm');
            // No we will check for those with the matching params.
            out.forEach((line) => {
                const match = regex.exec(line) || false;
                if (match) {
                    this.log.debug(`killing MSBuild.exe at pid: ${match[1]}`);
                    spawn.sync('taskkill', ['/pid', match[1], '/f', '/t']);
                }
                regex.lastIndex = 0;
            });
        } catch (e) {
            this.log.debug('kill MSBuild failed');
        }
    }

    /**
     * On Windows it waits for the app.asar in the packed app to be free (no file locks).
     * @returns {*}
     */
    wait() {
        if (this.currentContext.platform.nodeName !== 'win32') {
            return Promise.resolve();
        }
        const platformDir = `win-${this.currentContext.arch === 'ia32' ? 'ia32-' : ''}unpacked`;
        const appAsarPath = path.join(
            this.$.env.options.output,
            this.$.env.paths.installerDir,
            platformDir,
            'resources', 'app', 'app.asar'
        );
        let retries = 0;
        const self = this;
        return new Promise((resolve, reject) => {
            function check() {
                fs.open(appAsarPath, 'r+', (err, fd) => {
                    retries += 1;
                    if (err) {
                        if (err.code !== 'ENOENT') {
                            self.log.debug(`waiting for app.asar to be readable, ${'code' in err ? `currently reading it returns ${err.code}` : ''}`);
                            if (retries < 6) {
                                setTimeout(() => check(), 1000);
                            } else {
                                reject(`file is locked: ${appAsarPath}`);
                            }
                        } else {
                            resolve();
                        }
                    } else {
                        fs.closeSync(fd);
                        resolve();
                    }
                });
            }
            check();
        });
    }

    /**
     * Prepares the target object passed to the electron-builder.
     *
     * @returns {Map<Platform, Map<Arch, Array<string>>>}
     */
    prepareTargets() {
        let arch = this.$.env.options.ia32 ? 'ia32' : 'x64';
        arch = this.$.env.options.allArchs ? 'all' : arch;

        const targets = [];

        if (this.$.env.options.win) {
            targets.push(Platform.WINDOWS);
        }
        if (this.$.env.options.linux) {
            targets.push(Platform.LINUX);
        }
        if (this.$.env.options.mac) {
            targets.push(Platform.MAC);
        }

        if (targets.length === 0) {
            if (this.$.env.os.isWindows) {
                targets.push(Platform.WINDOWS);
            } else if (this.$.env.os.isLinux) {
                targets.push(Platform.LINUX);
            } else {
                targets.push(Platform.MAC);
            }
        }
        return createTargets(targets, null, arch);
    }

    async build() {
        const settings = this.$.desktop.getSettings();
        if (!('builderOptions' in settings)) {
            this.log.error(
                'no builderOptions in settings.json, aborting'
            );
            process.exit(1);
        }

        const builderOptions = Object.assign({}, settings.builderOptions);

        builderOptions.asar = false;
        builderOptions.npmRebuild = true;

        builderOptions.beforeBuild = this.beforeBuild.bind(this);
        builderOptions.afterPack = this.afterPack.bind(this);
        builderOptions.electronVersion = this.$.getElectronVersion();

        builderOptions.directories = {
            app: this.$.env.paths.electronApp.root,
            output: path.join(this.$.env.options.output, this.$.env.paths.installerDir)
        };

        try {
            this.log.debug('calling build from electron-builder');
            await build(Object.assign({
                targets: this.prepareTargets(),
                config: builderOptions
            }, settings.builderCliOptions));
        } catch (e) {
            this.log.error('error while building installer: ', e);
        }
    }

    /**
     * Moves node_modules out of the app because while the app will be packaged
     * we do not want it to be there.
     * @returns {Promise<any>}
     */
    moveNodeModulesOut() {
        return new Promise((resolve, reject) => {
            this.log.debug('moving node_modules out, because we have them already in' +
                ' app.asar');
            this.killMSBuild();
            removeDir(this.$.env.paths.electronApp.tmpNodeModules)
                .catch(e => reject(e))
                .then(() => {
                    shell.config.fatal = true;
                    shell.config.verbose = true;
                    try {
                        shell.mv(
                            this.$.env.paths.electronApp.nodeModules,
                            this.$.env.paths.electronApp.tmpNodeModules
                        );
                        shell.config.reset();
                        return this.wait();
                    } catch (e) {
                        shell.config.reset();
                        return Promise.reject(e);
                    }
                })
                .catch(e => reject(e))
                .then(() => removeDir(this.$.env.paths.electronApp.nodeModules, 1000))
                .catch(e => reject(e))
                .then(() => this.wait())
                .catch(e => reject(e))
                .then(() => resolve());
        });
    }
}
<commit_msg>Fix waiting for app.asar<commit_after>import { build, Platform, createTargets } from 'electron-builder';
import { installOrRebuild } from 'electron-builder-lib/out/util/yarn';
import { createLazyProductionDeps } from 'electron-builder-lib/out/util/packageDependencies';
import shell from 'shelljs';
import path from 'path';
import fs from 'fs';
import rimraf from 'rimraf';
import spawn from 'cross-spawn';
import Log from './log';

/**
 * Promisfied rimraf.
 *
 * @param {string} dirPath - path to the dir to be deleted
 * @param {number} delay - delay the task by ms
 * @returns {Promise<any>}
 */
function removeDir(dirPath, delay = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            rimraf(dirPath, {
                maxBusyTries: 100
            }, (err) => {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            });
        }, delay);
    });
}

/**
 * Wrapper for electron-builder.
 */
export default class InstallerBuilder {
    /**
     * @param {MeteorDesktop} $ - context
     *
     * @constructor
     */
    constructor($) {
        this.log = new Log('electronBuilder');
        this.$ = $;
        this.firstPass = true;
        this.lastRebuild = {};
        this.currentContext = null;
    }

    /**
     * Prepares the last rebuild object for electron-builder.
     *
     * @param {string} arch
     * @param {string} platform
     * @returns {Object}
     */
    prepareLastRebuildObject(arch, platform = process.platform) {
        const productionDeps = createLazyProductionDeps(this.$.env.paths.electronApp.root);
        this.lastRebuild = {
            frameworkInfo: { version: this.$.getElectronVersion(), useCustomDist: true },
            platform,
            arch,
            productionDeps
        };
        return this.lastRebuild;
    }

    /**
     * Calls npm rebuild from electron-builder.
     * @param {string} arch
     * @param {string} platform
     * @param {boolean} install
     * @returns {Promise}
     */
    async installOrRebuild(arch, platform = process.platform, install = false) {
        this.log.debug(`calling installOrRebuild from electron-builder for arch ${arch}`);
        this.prepareLastRebuildObject(arch, platform);
        await installOrRebuild(this.$.desktop.getSettings().builderOptions || {},
            this.$.env.paths.electronApp.root, this.lastRebuild, install);
    }

    /**
     * Callback invoked before build is made. Ensures that app.asar have the right rebuilt
     * node_modules.
     *
     * @param {Object} context
     * @returns {Promise}
     */
    beforeBuild(context) {
        this.currentContext = Object.assign({}, context);
        return new Promise((resolve, reject) => {
            const platformMatches = process.platform === context.platform.nodeName;
            const rebuild = platformMatches && context.arch !== this.lastRebuild.arch;
            if (!platformMatches) {
                this.log.warn('skipping dependencies rebuild because platform is different, if you have native ' +
                    'node modules as your app dependencies you should od the build on the target platform only');
            }

            if (!rebuild) {
                this.moveNodeModulesOut()
                    .catch(e => reject(e))
                    .then(() => setTimeout(() => resolve(false), 2000));
                // Timeout helps on Windows to clear the file locks.
            } else {
                // Lets rebuild the node_modules for different arch.
                this.installOrRebuild(context.arch, context.platform.nodeName)
                    .catch(e => reject(e))
                    .then(() => this.$.electronApp.installLocalNodeModules(context.arch))
                    .catch(e => reject(e))
                    .then(() => {
                        this.$.electronApp.scaffold.createAppRoot();
                        this.$.electronApp.scaffold.copySkeletonApp();
                        return this.$.electronApp.packSkeletonToAsar(
                            [
                                this.$.env.paths.electronApp.meteorAsar,
                                this.$.env.paths.electronApp.desktopAsar,
                                this.$.env.paths.electronApp.extracted
                            ]
                        );
                    })
                    .catch(e => reject(e))
                    .then(() => this.moveNodeModulesOut())
                    .catch(e => reject(e))
                    .then(() => resolve(false));
            }
        });
    }

    /**
     * Callback to be invoked after packing. Restores node_modules to the .desktop-build.
     * @returns {Promise}
     */
    afterPack() {
        return new Promise((resolve, reject) => {
            this.log.debug('moving node_modules back');
            // Move node_modules back.

            shell.config.fatal = true;
            try {
                shell.mv(
                    this.$.env.paths.electronApp.tmpNodeModules,
                    this.$.env.paths.electronApp.nodeModules
                );
            } catch (e) {
                reject(e);
                return;
            } finally {
                shell.config.reset();
            }

            if (this.firstPass) {
                this.firstPass = false;
            }
            this.log.debug('node_modules moved back');

            this.wait()
                .catch(e => reject(e))
                .then(() => resolve());
        });
    }

    /**
     * This command kills orphaned MSBuild.exe processes.
     * Sometime after native node_modules compilation they are still writing some logs,
     * prevent node_modules from being deleted.
     */
    killMSBuild() {
        if (this.currentContext.platform.nodeName !== 'win32') {
            return;
        }
        try {
            const out = spawn
                .sync(
                    'wmic',
                    ['process', 'where', 'caption="MSBuild.exe"', 'get', 'processid']
                )
                .stdout.toString('utf-8')
                .split('\n');

            const regex = new RegExp(/(\d+)/, 'gm');
            // No we will check for those with the matching params.
            out.forEach((line) => {
                const match = regex.exec(line) || false;
                if (match) {
                    this.log.debug(`killing MSBuild.exe at pid: ${match[1]}`);
                    spawn.sync('taskkill', ['/pid', match[1], '/f', '/t']);
                }
                regex.lastIndex = 0;
            });
        } catch (e) {
            this.log.debug('kill MSBuild failed');
        }
    }

    /**
     * On Windows it waits for the app.asar in the packed app to be free (no file locks).
     * @returns {*}
     */
    wait() {
        if (this.currentContext.platform.nodeName !== 'win32') {
            return Promise.resolve();
        }
        const platformDir = `win-${this.currentContext.arch === 'ia32' ? 'ia32-' : ''}unpacked`;
        const appAsarPath = path.join(
            this.$.env.options.output,
            this.$.env.paths.installerDir,
            platformDir,
            'resources', 'app', 'app.asar'
        );
        let retries = 0;
        const self = this;
        return new Promise((resolve, reject) => {
            function check() {
                fs.open(appAsarPath, 'r+', (err, fd) => {
                    retries += 1;
                    if (err) {
                        if (err.code !== 'ENOENT') {
                            self.log.debug(`waiting for app.asar to be readable, ${'code' in err ? `currently reading it returns ${err.code}` : ''}`);
                            if (retries < 6) {
                                setTimeout(() => check(), 4000);
                            } else {
                                reject(`file is locked: ${appAsarPath}`);
                            }
                        } else {
                            resolve();
                        }
                    } else {
                        fs.closeSync(fd);
                        resolve();
                    }
                });
            }
            check();
        });
    }

    /**
     * Prepares the target object passed to the electron-builder.
     *
     * @returns {Map<Platform, Map<Arch, Array<string>>>}
     */
    prepareTargets() {
        let arch = this.$.env.options.ia32 ? 'ia32' : 'x64';
        arch = this.$.env.options.allArchs ? 'all' : arch;

        const targets = [];

        if (this.$.env.options.win) {
            targets.push(Platform.WINDOWS);
        }
        if (this.$.env.options.linux) {
            targets.push(Platform.LINUX);
        }
        if (this.$.env.options.mac) {
            targets.push(Platform.MAC);
        }

        if (targets.length === 0) {
            if (this.$.env.os.isWindows) {
                targets.push(Platform.WINDOWS);
            } else if (this.$.env.os.isLinux) {
                targets.push(Platform.LINUX);
            } else {
                targets.push(Platform.MAC);
            }
        }
        return createTargets(targets, null, arch);
    }

    async build() {
        const settings = this.$.desktop.getSettings();
        if (!('builderOptions' in settings)) {
            this.log.error(
                'no builderOptions in settings.json, aborting'
            );
            process.exit(1);
        }

        const builderOptions = Object.assign({}, settings.builderOptions);

        builderOptions.asar = false;
        builderOptions.npmRebuild = true;

        builderOptions.beforeBuild = this.beforeBuild.bind(this);
        builderOptions.afterPack = this.afterPack.bind(this);
        builderOptions.electronVersion = this.$.getElectronVersion();

        builderOptions.directories = {
            app: this.$.env.paths.electronApp.root,
            output: path.join(this.$.env.options.output, this.$.env.paths.installerDir)
        };

        try {
            this.log.debug('calling build from electron-builder');
            await build(Object.assign({
                targets: this.prepareTargets(),
                config: builderOptions
            }, settings.builderCliOptions));
        } catch (e) {
            this.log.error('error while building installer: ', e);
        }
    }

    /**
     * Moves node_modules out of the app because while the app will be packaged
     * we do not want it to be there.
     * @returns {Promise<any>}
     */
    moveNodeModulesOut() {
        return new Promise((resolve, reject) => {
            this.log.debug('moving node_modules out, because we have them already in' +
                ' app.asar');
            this.killMSBuild();
            removeDir(this.$.env.paths.electronApp.tmpNodeModules)
                .catch(e => reject(e))
                .then(() => {
                    shell.config.fatal = true;
                    shell.config.verbose = true;
                    try {
                        shell.mv(
                            this.$.env.paths.electronApp.nodeModules,
                            this.$.env.paths.electronApp.tmpNodeModules
                        );
                        shell.config.reset();
                        return this.wait();
                    } catch (e) {
                        shell.config.reset();
                        return Promise.reject(e);
                    }
                })
                .catch(e => reject(e))
                .then(() => removeDir(this.$.env.paths.electronApp.nodeModules, 1000))
                .catch(e => reject(e))
                .then(() => this.wait())
                .catch(e => reject(e))
                .then(() => resolve());
        });
    }
}
<|endoftext|>


<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";

define(['ui', 'records', 'map', 'utils', 'settings', 'config', './tracks'], function(ui, records,  map, utils, settings, config, tracks){
    var currentGpsAnnotation;

    /**
     * GPS capture form page.
     */
    var annotateGpsPage = function(){
        var colour = "red";
        var defcolour = localStorage.getItem(tracks.COLOUR_INDEX);
        if(defcolour){
            colour = defcolour;
        }
        else{
            localStorage.setItem(tracks.COLOUR_INDEX, 'red');
        }

        // we need to add colour picker input dynamically otherwise
        // JQM will attempt to format the input element
        $('#annotate-gps-colour-pick').append('<input id="annotate-gps-colour-pick-input" type="color" name="color" />');

        utils.appendDateTimeToInput("#annotate-gps-form-title");

        $("#annotate-gps-colour-pick-input").spectrum({
            showPalette: true,
            showPaletteOnly: true,
            color: colour,
            change: function(color){
                localStorage.setItem(tracks.COLOUR_INDEX, color.toString());
            },
            palette: [
                ['red', 'orange', 'yellow', 'green'],
                ['blue', 'pink', 'white', 'black']
            ]
        });

        // listen on start button
        $('#annotate-gps-form-ok').click(function(event){
            $('#annotate-gps-form').submit();
        });

        // form submitted
        $('#annotate-gps-form').submit(function(event){
            if($('#annotate-gps-form-title').val().length === 0){
                $('#annotate-gps-form-title').addClass('ui-focus');
                utils.inform('Required field not populated');
            }
            else{
                currentGpsAnnotation = {
                    'record':{
                        'editor': 'track.edtr',
                        'name': $('#annotate-gps-form-title').val(),
                        'fields': [
                            {
                                'id': 'fieldcontain-textarea-1',
                                'val': $('#annotate-gps-form-description').val(),
                                'label': 'Description',
                            },
                            {
                                // track currently must be second element,
                                // see showGPSTrack
                                'id': 'fieldcontain-track-1',
                                'val': '',
                                'label': 'Track',
                                'style': {
                                    strokeColor: localStorage.getItem(tracks.COLOUR_INDEX),
                                    strokeWidth: 5,
                                    strokeOpacity: 1
                                }
                            }
                        ],
                    },
                    'isSynced': false,
                    'rate': $('#annotate-gps-form-rate').val()
                };

                utils.hideKeyboard();

                // kick off capture
                tracks.gpsTrack(currentGpsAnnotation, debugGPS());

                $.mobile.changePage('gps-capture.html');
            }

            return false;
        });
    };

    /**
     * Should GPS tracking be run in debug mode?
     */
    var debugGPS = function(){
        return settings.get('debug-gps') === 'on';
    };

    /**
     * Initialise GPS capture page.
     */
    var gpsCapturePage = function(){
        ui.mapPage('gpscapture-map');

        var changeToResume = function(){
            $("#gpscapture-pause-play .ui-btn-text").text('Resume');
            $("#gpscapture-pause-play .ui-icon").css('background-image',
                                                     'url("css/images/play.png")');
        };

        var gotoPage = function(page){
            if(typeof(config.leave_after_gps_save_discard) === 'undefined' ||
               utils.str2bool(config.go_to_map_after_gps_save)){
                $.mobile.changePage(map);
            }
        };

        if(tracks.gpsTrackPaused()){
            changeToResume();
        }

        // save GPS route
        $('#gpscapture-confirm-save').click(function(e){
            currentGpsAnnotation = undefined;
            tracks.gpsCaptureComplete();
            gotoPage('map.html');
        });

        // cancel GPS route save
        $('#gpscapture-confirm-cancel').click(function(){
            tracks.gpsTrack();
        });

        // pause/resume GPS track button
        $('#gpscapture-pause-play').click(function(){
            if($("#gpscapture-pause-play").text().trim() === 'Pause'){
                tracks.gpsTrackPause();
                changeToResume();
            }
            else{
                tracks.gpsTrackPlay(currentGpsAnnotation.rate, debugGPS());
                $("#gpscapture-pause-play .ui-btn-text").text('Pause');
                $("#gpscapture-pause-play .ui-icon").css(
                    'background-image',
                    'url("plugins/gps-tracking/css/images/pause.png")');
            }

            $('#gpscapture-pause-play').removeClass('ui-btn-active');
        });

        // toogle track visibility
        $('#gpscapture-toggle-route').click(function(){
            tracks.gpsTrackToggle();
            $('#gpscapture-toggle-route').removeClass('ui-btn-active');
        });

        // discard track
        $('#gpscapture-confirm-discard').click(function(){
            currentGpsAnnotation = undefined;
            tracks.gpsCaptureDiscard();
            $('#gpscapture-toggle-route').removeClass('ui-btn-active');
            gotoPage('index.html');
        });

        if(config.show_records_on_gps_tracking_page){
            map.showRecordsLayer();
        }

        map.hideAnnotateLayer();
    };

    /**
     * checkPopups
     * If an annotation has been stored in sessionStorage
     * with key annotationPopup, the appropriate popup
     * will be shown automatically
     *
     */
    var checkPopups = function() {

        var a = sessionStorage.getItem('annotationPopup');
        if (a !== 'undefined') {

            var annotation = $.parseJSON(a);

            if (annotation) {
                map.createPopup(annotation);
                $('#map-record-popup').popup('open');
            }

            // Clean up
            sessionStorage.removeItem('annotationPopup');
        }
    };

    // load spectrum js and css files for colour picker
    $.getScript('js/ext/spectrum.js');
    $('head').prepend('<link rel="stylesheet" href="css/ext/spectrum.css" type="text/css" />');

    // load gpx styles
    $('head').prepend('<link rel="stylesheet" href="plugins/gps-tracking/css/style.css" type="text/css" />');

    // initial annotate page form
    $(document).on('pageinit', '#annotate-gps-page', annotateGpsPage);

    // the page that the track runs on
    $(document).on('pageinit', '#gpscapture-page', gpsCapturePage);

    // Check if annotation popup should be shown automatically
    $(document).on('pageshow', '#gpscapture-page', checkPopups);
    $(document).on(
        'pageshow',
        '#gpscapture-page',
        function(){
            map.updateSize();
        }
    );

    // show / hide gps track running icon
    $(document).on(
        'pagebeforeshow',
        'div[data-role="page"]',
        function(event){
            if(tracks.gpsTrackStarted()){
                $('.gpstrack-running').show();
            }
            else{
                $('.gpstrack-running').hide();
            }
        }
    );

    // click on gps capture running icon
    $(document).on(
        'vmousedown',
        '.gpstrack-running',
        function(event){
            // timout hack prevents the clicking on the button on the
            // same position on the next page
            setTimeout(function(){
                $.mobile.changePage('gps-capture.html');
                event.stopPropagation();
            }, 400);

            return false;
        }
    );

});
<commit_msg>debugGPS function not picking up debug-gps toggle on #2<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";

define(['ui', 'records', 'map', 'utils', 'settings', 'config', './tracks'], function(ui, records,  map, utils, settings, config, tracks){
    var currentGpsAnnotation;

    /**
     * GPS capture form page.
     */
    var annotateGpsPage = function(){
        var colour = "red";
        var defcolour = localStorage.getItem(tracks.COLOUR_INDEX);
        if(defcolour){
            colour = defcolour;
        }
        else{
            localStorage.setItem(tracks.COLOUR_INDEX, 'red');
        }

        // we need to add colour picker input dynamically otherwise
        // JQM will attempt to format the input element
        $('#annotate-gps-colour-pick').append('<input id="annotate-gps-colour-pick-input" type="color" name="color" />');

        utils.appendDateTimeToInput("#annotate-gps-form-title");

        $("#annotate-gps-colour-pick-input").spectrum({
            showPalette: true,
            showPaletteOnly: true,
            color: colour,
            change: function(color){
                localStorage.setItem(tracks.COLOUR_INDEX, color.toString());
            },
            palette: [
                ['red', 'orange', 'yellow', 'green'],
                ['blue', 'pink', 'white', 'black']
            ]
        });

        // listen on start button
        $('#annotate-gps-form-ok').click(function(event){
            $('#annotate-gps-form').submit();
        });

        // form submitted
        $('#annotate-gps-form').submit(function(event){
            if($('#annotate-gps-form-title').val().length === 0){
                $('#annotate-gps-form-title').addClass('ui-focus');
                utils.inform('Required field not populated');
            }
            else{
                currentGpsAnnotation = {
                    'record':{
                        'editor': 'track.edtr',
                        'name': $('#annotate-gps-form-title').val(),
                        'fields': [
                            {
                                'id': 'fieldcontain-textarea-1',
                                'val': $('#annotate-gps-form-description').val(),
                                'label': 'Description',
                            },
                            {
                                // track currently must be second element,
                                // see showGPSTrack
                                'id': 'fieldcontain-track-1',
                                'val': '',
                                'label': 'Track',
                                'style': {
                                    strokeColor: localStorage.getItem(tracks.COLOUR_INDEX),
                                    strokeWidth: 5,
                                    strokeOpacity: 1
                                }
                            }
                        ],
                    },
                    'isSynced': false,
                    'rate': $('#annotate-gps-form-rate').val()
                };

                utils.hideKeyboard();

                // kick off capture
                tracks.gpsTrack(currentGpsAnnotation, debugGPS());
                $.mobile.changePage('gps-capture.html');
            }

            return false;
        });
    };

    /**
     * Should GPS tracking be run in debug mode?
     */
    var debugGPS = function(){
        var gpsDebug = settings.get('debug-gps') ;
        return settings.get('debug-gps').val === 'on';
    };

    /**
     * Initialise GPS capture page.
     */
    var gpsCapturePage = function(){
        ui.mapPage('gpscapture-map');

        var changeToResume = function(){
            $("#gpscapture-pause-play .ui-btn-text").text('Resume');
            $("#gpscapture-pause-play .ui-icon").css('background-image',
                                                     'url("css/images/play.png")');
        };

        var gotoPage = function(page){
            if(typeof(config.leave_after_gps_save_discard) === 'undefined' ||
               utils.str2bool(config.go_to_map_after_gps_save)){
                $.mobile.changePage(map);
            }
        };

        if(tracks.gpsTrackPaused()){
            changeToResume();
        }

        // save GPS route
        $('#gpscapture-confirm-save').click(function(e){
            currentGpsAnnotation = undefined;
            tracks.gpsCaptureComplete();
            gotoPage('map.html');
        });

        // cancel GPS route save
        $('#gpscapture-confirm-cancel').click(function(){
            tracks.gpsTrack();
        });

        // pause/resume GPS track button
        $('#gpscapture-pause-play').click(function(){
            if($("#gpscapture-pause-play").text().trim() === 'Pause'){
                tracks.gpsTrackPause();
                changeToResume();
            }
            else{
                tracks.gpsTrackPlay(currentGpsAnnotation.rate, debugGPS());
                $("#gpscapture-pause-play .ui-btn-text").text('Pause');
                $("#gpscapture-pause-play .ui-icon").css(
                    'background-image',
                    'url("plugins/gps-tracking/css/images/pause.png")');
            }

            $('#gpscapture-pause-play').removeClass('ui-btn-active');
        });

        // toogle track visibility
        $('#gpscapture-toggle-route').click(function(){
            tracks.gpsTrackToggle();
            $('#gpscapture-toggle-route').removeClass('ui-btn-active');
        });

        // discard track
        $('#gpscapture-confirm-discard').click(function(){
            currentGpsAnnotation = undefined;
            tracks.gpsCaptureDiscard();
            $('#gpscapture-toggle-route').removeClass('ui-btn-active');
            gotoPage('index.html');
        });

        if(config.show_records_on_gps_tracking_page){
            map.showRecordsLayer();
        }

        map.hideAnnotateLayer();
    };

    /**
     * checkPopups
     * If an annotation has been stored in sessionStorage
     * with key annotationPopup, the appropriate popup
     * will be shown automatically
     *
     */
    var checkPopups = function() {

        var a = sessionStorage.getItem('annotationPopup');
        if (a !== 'undefined') {

            var annotation = $.parseJSON(a);

            if (annotation) {
                map.createPopup(annotation);
                $('#map-record-popup').popup('open');
            }

            // Clean up
            sessionStorage.removeItem('annotationPopup');
        }
    };

    // load spectrum js and css files for colour picker
    $.getScript('js/ext/spectrum.js');
    $('head').prepend('<link rel="stylesheet" href="css/ext/spectrum.css" type="text/css" />');

    // load gpx styles
    $('head').prepend('<link rel="stylesheet" href="plugins/gps-tracking/css/style.css" type="text/css" />');

    // initial annotate page form
    $(document).on('pageinit', '#annotate-gps-page', annotateGpsPage);

    // the page that the track runs on
    $(document).on('pageinit', '#gpscapture-page', gpsCapturePage);

    // Check if annotation popup should be shown automatically
    $(document).on('pageshow', '#gpscapture-page', checkPopups);
    $(document).on(
        'pageshow',
        '#gpscapture-page',
        function(){
            map.updateSize();
        }
    );

    // show / hide gps track running icon
    $(document).on(
        'pagebeforeshow',
        'div[data-role="page"]',
        function(event){
            if(tracks.gpsTrackStarted()){
                $('.gpstrack-running').show();
            }
            else{
                $('.gpstrack-running').hide();
            }
        }
    );

    // click on gps capture running icon
    $(document).on(
        'vmousedown',
        '.gpstrack-running',
        function(event){
            // timout hack prevents the clicking on the button on the
            // same position on the next page
            setTimeout(function(){
                $.mobile.changePage('gps-capture.html');
                event.stopPropagation();
            }, 400);

            return false;
        }
    );

});
<|endoftext|>


<commit_before>var map,
    MAP_NODE           = $("#canvas").get(0),
    $resetBtn          = $('#control-reset'),
    $undoBtn           = $('#control-undo'),
    mapOptions         = {
            zoom: 13,
            mapTypeId: google.maps.MapTypeId.TERRAIN,
            mapTypeControl: true,
            mapTypeControlOptions: {
                style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
                position: google.maps.ControlPosition.LEFT_TOP
            },
            panControl: true,
            panControlOptions: {
                position: google.maps.ControlPosition.LEFT_CENTER
            },
            zoomControl: true,
            zoomControlOptions: {
                position: google.maps.ControlPosition.LEFT_CENTER
            }
        },
    polyline           = {},
    path               = [], // cache points
    segments           = [], // cache calculated segments
    directions         = [], // cache calculated directions
    markers            = [], // cache point marker objects
    TRAVEL_MODE        = google.maps.DirectionsTravelMode.BICYCLING,
    UNIT_SYSTEM        = google.maps.UnitSystem.IMPERIAL,
    AVOID_HIGHWAYS     = true,
    OPTIMIZE_WAYPOINTS = false,
    PROVIDE_ROUTE_ALTERNATIVES = false,
    DIRECTIONS_SERVICE = new google.maps.DirectionsService(),
    ERROR_STATUSES     = [
        {
            constant    : "INVALID_REQUEST",
            description : "The DirectionsRequest provided was invalid.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "MAX_WAYPOINTS_EXCEEDED",
            description : "Too many DirectionsWaypoints were provided in the DirectionsRequest. The total allowed waypoints is 8, plus the origin and destination. Maps API for Business customers are allowed 23 waypoints, plus the origin, and destination.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "NOT_FOUND",
            description : "At least one of the origin, destination, or waypoints could not be geocoded.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "OVER_QUERY_LIMIT",
            description : "The webpage has gone over the requests limit in too short a period of time.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "REQUEST_DENIED",
            description : "The webpage is not allowed to use the directions service.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "UNKNOWN_ERROR",
            description : "A directions request could not be processed due to a server error. The request may succeed if you try again.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "ZERO_RESULTS",
            description : "No route could be found between the origin and destination.",
            remedy      : "Try not crossing over country boundaries."
        }
    ];

/**
 * ROUTE METHODS
 * {method} createRoute
 * {method} destroyRoute
 * {method} extendRoute
 * {method} curtailRoute
 */

function createRoute(location) {

    console.log('== CREATE ROUTE ==');

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to createRoute()');
        return;
    }

    // Create a marker at the clicked location.
    _createMarker(location);

    // Set the clicked location, the path's origin, to be the first point.
    path.push(location);

    // Create the polyline and draw the path.
    polyline = new google.maps.Polyline({ map: map });
    polyline.setPath(path);
}

function destroyRoute() {

    console.log('== DESTROY ROUTE ==');

    // If the user clicks Reset button before creating the route
    if (polyline.setPath === undefined) {
        console.log('[DayTrip] Error: unable to destroy route that doesn\'t exist.');
        return;
    }

    // Empty all points from path, then erase the polyline.
    path = [];
    polyline.setPath(path);

    // Remove the markers and underlying marker data.
    for (var i = markers.length; i > 0; i--) {
        _destroyMarker(i - 1);
    }
}

function extendRoute(location) {

    console.log('== EXTEND ROUTE ==');

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to extendRoute()');
        return;
    }

    // Calculate directions between the previous point and the current point.
    DIRECTIONS_SERVICE.route({

        origin:                   path[path.length - 1],

        destination:              location,

        travelMode:               TRAVEL_MODE,

        unitSystem:               UNIT_SYSTEM,

        avoidHighways:            AVOID_HIGHWAYS,

        optimizeWaypoints:        OPTIMIZE_WAYPOINTS,

        provideRouteAlternatives: PROVIDE_ROUTE_ALTERNATIVES

    }, function(result, status) {

        if (status === "OK") {

            _addSegment(result);

            getDirections();

        } else {

            for (var s = 0, slen = ERROR_STATUSES.length; s < slen; s++) {
                if (status === ERROR_STATUSES[s]['constant']) {
                    throwError(ERROR_STATUSES[s], result);
                    return;
                }
            }
        }
    });
}

function curtailRoute() {

    console.log('== CURTAIL ROUTE ==');

    _removeSegment();
}

/**
 * DIRECTIONS METHODS
 * {method} getDirections
 * {method} _displayDirections
 */

function getDirections() {

    /* Google Maps API limit is 8 waypoints plus origin and destination. :( */
    var origin      = path[0],
        destination = path[path.length - 1],
        intervals   = _.uniq([
            Math.round(0.1111 * (path.length - 1)),
            Math.round(0.2222 * (path.length - 1)),
            Math.round(0.3333 * (path.length - 1)),
            Math.round(0.4444 * (path.length - 1)),
            Math.round(0.5555 * (path.length - 1)),
            Math.round(0.6666 * (path.length - 1)),
            Math.round(0.7777 * (path.length - 1)),
            Math.round(0.8888 * (path.length - 1))
        ])
        waypoints = [];

    for (var i = 0, len = intervals.length; i < len; i++) {
        waypoints[i] = {
            location: path[intervals[i]],
            stopover: false
        };
    }

    DIRECTIONS_SERVICE.route({

        origin:                   origin,

        destination:              destination,

        waypoints:                waypoints,

        travelMode:               TRAVEL_MODE,

        unitSystem:               UNIT_SYSTEM,

        avoidHighways:            AVOID_HIGHWAYS,

        optimizeWaypoints:        OPTIMIZE_WAYPOINTS,

        provideRouteAlternatives: PROVIDE_ROUTE_ALTERNATIVES

    }, function(result, status) {

        if (status === "OK") {

            _displayDirections(result);

        } else {

            for (var s = 0, slen = ERROR_STATUSES.length; s < slen; s++) {
                if (status === ERROR_STATUSES[s]['constant']) {
                    throwError(ERROR_STATUSES[s], result);
                    return;
                }
            }
        }
    });
}

function _displayDirections(result) {

    // Argument sanity check.
    if (!result || typeof(result) !== 'object') {
        console.log('[DayTrip] Error: Invalid result passed to _displayDirections()');
        return;
    }

    // Assumptions: only one route, only one leg.
    var leg           = result.routes[0].legs[0],
        steps         = leg['steps'] || [],
        origin        = leg['start_address'],
        destination   = leg['end_address'],
        totalDistance = leg['distance']['text'],
        totalDuration = leg['duration']['text'],
        stepsHTML     = '';

    stepsHTML = '
';
    for (var s = 0, slen = steps.length; s < slen; s++) {
        stepsHTML += '
' + steps[s]['instructions'] + ' (' + steps[s]['distance']['text'] + ')
';
    }
    stepsHTML += '
';

    $('#origin').text('From ' + origin);
    $('#destination').text('To ' + destination);
    $('#totalDistance').text(totalDistance);
    $('#totalDuration').text(totalDuration);
    $('#directions').html(stepsHTML);
}

/**
 * SEGMENT METHODS
 * {method} _addSegment
 * {method} _removeSegment
 */

function _addSegment(result) {

    // Argument sanity check.
    if (!result || typeof(result) !== 'object') {
        console.log('[DayTrip] Error: Invalid result passed to _addSegment()');
        return;
    }

    var segment = result.routes[0].overview_path        || [],
        seg_pts = result.routes[0].overview_path.length || 0;

    // If the path contains more than one point, erase the old final point's marker.
    if (path.length > 1) {
        _eraseMarker(markers.length - 1);
    }

    segments.push(seg_pts);

    // Add this segment's points to the path array.
    path = path.concat(segment);

    // Place a marker at the new final point.
    _createMarker(path[path.length - 1]);

    // Redraw the path.
    polyline.setPath(path);
}

function _removeSegment() {

    console.log('== _removeSegment() ==');

    var pointsToRemove = segments[segments.length - 1];

    // If no segments to remove, reset the route.
    if (segments.length === 0) {

        destroyRoute();

    } else {

        // Destroy the last marker in the marker's array
        _destroyMarker(markers.length - 1);

        // Remove the last (segment's number of) items from the path array
        path.splice(-pointsToRemove, pointsToRemove);

        // If path still contains more than one point, draw a marker at the new last point.
        if (path.length > 1) {
            _drawMarker(markers.length - 1);
        }

        // Redraw the path.
        polyline.setPath(path);

        // Remove the last item in the segment array.
        segments.splice(-1, 1);
    }
}

/**
 * MARKER METHODS
 * {method} _createMarker
 * {method} _destroyMarker
 * {method} _drawMarker
 * {method} _eraseMarker
 */

function _createMarker(location) {

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to _createMarker()');
        return;
    }

    // Draw a marker at the provided location.
    var marker = new google.maps.Marker({
            position: location,
            map: map
        });

    // Append this marker to the markers array.
    markers.push(marker);
}

function _destroyMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _destroyMarker()');
        return;
    }

    // Erase this marker from the map.
    _eraseMarker(index);

    // Remove this marker's data from the markers array.
    markers.splice(index, 1);
}

function _drawMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _drawMarker()');
        return;
    }

    // Draw this marker on the map.
    markers[index].setMap(map);
}

function _eraseMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _eraseMarker()');
        return;
    }

    // Erase this marker from the map.
    markers[index].setMap();
}

/**
 * ERROR METHOD
 */

function throwError(status, result) {

    console.log('[DayTrip] ERROR.');
    console.log('|  type  |', status['constant']);
    console.log('|  desc  |', status['description']);
    console.log('| remedy |', status['remedy']);
    // console.log('debug result object from google', result);
}

/**
 * init()
 */

function init() {

    console.log('== INIT ==');

    // Set map center
    mapOptions['center'] = new google.maps.LatLng(40.25275, -108.64038);

    // Assign map to HTML element
    map = new google.maps.Map(MAP_NODE, mapOptions);

    // Kick off controls
    $resetBtn.on('click', function(event) {
        destroyRoute();
    });

    $undoBtn.on('click', function(event) {
        curtailRoute();
    });

    // On click
    google.maps.event.addListener(map, "click", function(event) {

        var location = event.latLng || '';

        if (path.length == 0) {
            createRoute(location);
        } else {
            extendRoute(location);
        }

    });
}

// Kickoff
google.maps.event.addDomListener(window, 'load', init);<commit_msg>Adds logging function for future debugging.<commit_after>var map,
    MAP_NODE           = $("#canvas").get(0),
    $resetBtn          = $('#control-reset'),
    $undoBtn           = $('#control-undo'),
    mapOptions         = {
            zoom: 13,
            mapTypeId: google.maps.MapTypeId.TERRAIN,
            mapTypeControl: true,
            mapTypeControlOptions: {
                style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
                position: google.maps.ControlPosition.LEFT_TOP
            },
            panControl: true,
            panControlOptions: {
                position: google.maps.ControlPosition.LEFT_CENTER
            },
            zoomControl: true,
            zoomControlOptions: {
                position: google.maps.ControlPosition.LEFT_CENTER
            }
        },
    polyline           = {},
    path               = [], // cache points
    segments           = [], // cache calculated segments
    directions         = [], // cache calculated directions
    markers            = [], // cache point marker objects
    TRAVEL_MODE        = google.maps.DirectionsTravelMode.BICYCLING,
    UNIT_SYSTEM        = google.maps.UnitSystem.IMPERIAL,
    AVOID_HIGHWAYS     = true,
    OPTIMIZE_WAYPOINTS = false,
    PROVIDE_ROUTE_ALTERNATIVES = false,
    DIRECTIONS_SERVICE = new google.maps.DirectionsService(),
    ERROR_STATUSES     = [
        {
            constant    : "INVALID_REQUEST",
            description : "The DirectionsRequest provided was invalid.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "MAX_WAYPOINTS_EXCEEDED",
            description : "Too many DirectionsWaypoints were provided in the DirectionsRequest. The total allowed waypoints is 8, plus the origin and destination. Maps API for Business customers are allowed 23 waypoints, plus the origin, and destination.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "NOT_FOUND",
            description : "At least one of the origin, destination, or waypoints could not be geocoded.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "OVER_QUERY_LIMIT",
            description : "The webpage has gone over the requests limit in too short a period of time.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "REQUEST_DENIED",
            description : "The webpage is not allowed to use the directions service.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "UNKNOWN_ERROR",
            description : "A directions request could not be processed due to a server error. The request may succeed if you try again.",
            remedy      : "Email ruthie@cyclingcoder.com."
        },
        {
            constant    : "ZERO_RESULTS",
            description : "No route could be found between the origin and destination.",
            remedy      : "Try not crossing over country boundaries."
        }
    ];

/**
 * ROUTE METHODS
 * {method} createRoute
 * {method} destroyRoute
 * {method} extendRoute
 * {method} curtailRoute
 */

function createRoute(location) {

    console.log('== CREATE ROUTE ==');

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to createRoute()');
        return;
    }

    // Create a marker at the clicked location.
    _createMarker(location);

    // Set the clicked location, the path's origin, to be the first point.
    path.push(location);

    // Create the polyline and draw the path.
    polyline = new google.maps.Polyline({ map: map });
    polyline.setPath(path);
}

function destroyRoute() {

    console.log('== DESTROY ROUTE ==');

    // If the user clicks Reset button before creating the route
    if (polyline.setPath === undefined) {
        console.log('[DayTrip] Error: unable to destroy route that doesn\'t exist.');
        return;
    }

    // Empty all points from path, then erase the polyline.
    path = [];
    polyline.setPath(path);

    // Remove the markers and underlying marker data.
    for (var i = markers.length; i > 0; i--) {
        _destroyMarker(i - 1);
    }
}

function extendRoute(location) {

    console.log('== EXTEND ROUTE ==');

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to extendRoute()');
        return;
    }

    // Calculate directions between the previous point and the current point.
    DIRECTIONS_SERVICE.route({

        origin:                   path[path.length - 1],

        destination:              location,

        travelMode:               TRAVEL_MODE,

        unitSystem:               UNIT_SYSTEM,

        avoidHighways:            AVOID_HIGHWAYS,

        optimizeWaypoints:        OPTIMIZE_WAYPOINTS,

        provideRouteAlternatives: PROVIDE_ROUTE_ALTERNATIVES

    }, function(result, status) {

        if (status === "OK") {

            _addSegment(result);

            getDirections();

        } else {

            for (var s = 0, slen = ERROR_STATUSES.length; s < slen; s++) {
                if (status === ERROR_STATUSES[s]['constant']) {
                    throwError(ERROR_STATUSES[s], result);
                    return;
                }
            }
        }
    });
}

function curtailRoute() {

    console.log('== CURTAIL ROUTE ==');

    _removeSegment();
}

/**
 * DIRECTIONS METHODS
 * {method} getDirections
 * {method} _displayDirections
 */

function getDirections() {

    /* Google Maps API limit is 8 waypoints plus origin and destination. :( */
    var origin      = path[0],
        destination = path[path.length - 1],
        intervals   = _.uniq([
            Math.round(0.1111 * (path.length - 1)),
            Math.round(0.2222 * (path.length - 1)),
            Math.round(0.3333 * (path.length - 1)),
            Math.round(0.4444 * (path.length - 1)),
            Math.round(0.5555 * (path.length - 1)),
            Math.round(0.6666 * (path.length - 1)),
            Math.round(0.7777 * (path.length - 1)),
            Math.round(0.8888 * (path.length - 1))
        ])
        waypoints = [];

    for (var i = 0, len = intervals.length; i < len; i++) {
        waypoints[i] = {
            location: path[intervals[i]],
            stopover: false
        };
    }

    DIRECTIONS_SERVICE.route({

        origin:                   origin,

        destination:              destination,

        waypoints:                waypoints,

        travelMode:               TRAVEL_MODE,

        unitSystem:               UNIT_SYSTEM,

        avoidHighways:            AVOID_HIGHWAYS,

        optimizeWaypoints:        OPTIMIZE_WAYPOINTS,

        provideRouteAlternatives: PROVIDE_ROUTE_ALTERNATIVES

    }, function(result, status) {

        if (status === "OK") {

            _displayDirections(result);

        } else {

            for (var s = 0, slen = ERROR_STATUSES.length; s < slen; s++) {
                if (status === ERROR_STATUSES[s]['constant']) {
                    throwError(ERROR_STATUSES[s], result);
                    return;
                }
            }
        }
    });
}

function _displayDirections(result) {

    // Argument sanity check.
    if (!result || typeof(result) !== 'object') {
        console.log('[DayTrip] Error: Invalid result passed to _displayDirections()');
        return;
    }

    // Assumptions: only one route, only one leg.
    var leg           = result.routes[0].legs[0],
        steps         = leg['steps'] || [],
        origin        = leg['start_address'],
        destination   = leg['end_address'],
        totalDistance = leg['distance']['text'],
        totalDuration = leg['duration']['text'],
        stepsHTML     = '';

    stepsHTML = '
';
    for (var s = 0, slen = steps.length; s < slen; s++) {
        stepsHTML += '
' + steps[s]['instructions'] + ' (' + steps[s]['distance']['text'] + ')
';
    }
    stepsHTML += '
';

    $('#origin').text('From ' + origin);
    $('#destination').text('To ' + destination);
    $('#totalDistance').text(totalDistance);
    $('#totalDuration').text(totalDuration);
    $('#directions').html(stepsHTML);
}

/**
 * SEGMENT METHODS
 * {method} _addSegment
 * {method} _removeSegment
 */

function _addSegment(result) {

    // Argument sanity check.
    if (!result || typeof(result) !== 'object') {
        console.log('[DayTrip] Error: Invalid result passed to _addSegment()');
        return;
    }

    var segment = result.routes[0].overview_path        || [],
        seg_pts = result.routes[0].overview_path.length || 0;

    // If the path contains more than one point, erase the old final point's marker.
    if (path.length > 1) {
        _eraseMarker(markers.length - 1);
    }

    segments.push(seg_pts);

    // Add this segment's points to the path array.
    path = path.concat(segment);

    // Place a marker at the new final point.
    _createMarker(path[path.length - 1]);

    // Redraw the path.
    polyline.setPath(path);
}

function _removeSegment() {

    var pointsToRemove = segments[segments.length - 1];

    // If no segments to remove, reset the route.
    if (segments.length === 0) {

        destroyRoute();

    } else {

        // Destroy the last marker in the marker's array
        _destroyMarker(markers.length - 1);

        // Remove the last (segment's number of) items from the path array
        path.splice(-pointsToRemove, pointsToRemove);

        // If path still contains more than one point, draw a marker at the new last point.
        if (path.length > 1) {
            _drawMarker(markers.length - 1);
        }

        // Redraw the path.
        polyline.setPath(path);

        // Remove the last item in the segment array.
        segments.splice(-1, 1);
    }
}

/**
 * MARKER METHODS
 * {method} _createMarker
 * {method} _destroyMarker
 * {method} _drawMarker
 * {method} _eraseMarker
 */

function _createMarker(location) {

    // Argument sanity check.
    if (!location || typeof(location) !== 'object') {
        console.log('[DayTrip] Error: Invalid location passed to _createMarker()');
        return;
    }

    // Draw a marker at the provided location.
    var marker = new google.maps.Marker({
            position: location,
            map: map
        });

    // Append this marker to the markers array.
    markers.push(marker);
}

function _destroyMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _destroyMarker()');
        return;
    }

    // Erase this marker from the map.
    _eraseMarker(index);

    // Remove this marker's data from the markers array.
    markers.splice(index, 1);
}

function _drawMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _drawMarker()');
        return;
    }

    // Draw this marker on the map.
    markers[index].setMap(map);
}

function _eraseMarker(index) {

    // Argument sanity check.
    if (index === undefined || typeof(index) !== 'number' || markers[index] === undefined) {
        console.log('[DayTrip] Error: Invalid index passed to _eraseMarker()');
        return;
    }

    // Erase this marker from the map.
    markers[index].setMap();
}

/**
 * ERROR METHOD
 */

function throwError(status, result) {

    console.log('[DayTrip] ERROR.');
    console.log('|  type  |', status['constant']);
    console.log('|  desc  |', status['description']);
    console.log('| remedy |', status['remedy']);
    // console.log('debug result object from google', result);
}

function __LOG(caller) {
    console.log('LOG FROM', caller);
    console.log('path',path,'path.length',path.length);
    console.log('segments',segments,'segments.length',segments.length);
    console.log('directions',directions,'directions.length',directions.length);
    console.log('markers',markers,'markers.length',markers.length);
}

/**
 * init()
 */

function init() {

    console.log('== INIT ==');

    // Set map center
    mapOptions['center'] = new google.maps.LatLng(40.25275, -108.64038);

    // Assign map to HTML element
    map = new google.maps.Map(MAP_NODE, mapOptions);

    // Kick off controls
    $resetBtn.on('click', function(event) {
        destroyRoute();
    });

    $undoBtn.on('click', function(event) {
        curtailRoute();
    });

    // On click
    google.maps.event.addListener(map, "click", function(event) {

        var location = event.latLng || '';

        if (path.length == 0) {
            createRoute(location);
        } else {
            extendRoute(location);
        }

    });
}

// Kickoff
google.maps.event.addDomListener(window, 'load', init);<|endoftext|>