wLib

A library for WME script developers.

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greasyfork.org/scripts/9794/106259/wLib.js

// ==UserScript==
// @name            wLib
// @description     A library for WME script developers.
// @version         1.0.7
// @author          SAR85
// @copyright       SAR85
// @license         CC BY-NC-ND
// @grant           none
// @include         https://www.waze.com/editor/*
// @include         https://www.waze.com/*/editor/*
// @include         https://editor-beta.waze.com/*
// @namespace       https://greasyfork.org/users/9321
// ==/UserScript==

/* global W */
/* global OL */
/* global wLib */

(function () {
    function bootstrap(tries) {
        tries = tries || 1;

        if (window.$ &&
            window.Backbone &&
            window._ &&
            window.OL &&
            window.W &&
            window.W.map &&
            window.W.accelerators &&
            window.W.model) {

            init();

        } else if (tries < 1000) {

            setTimeout(function () {
                bootstrap(tries++);
            }, 200);

        }
    }

    bootstrap();

    function init() {
        var oldLib = window.wLib;
        var root = this;
        
        /**
         * The wLib namespace.
         * @namespace {Object} wLib
         * @global
         */
        var wLib = {};
    
        /**
         * The current wLib version.
         * @member {string} wLib.VERSION
         */
        wLib.VERSION = '1.0.7';
        
        /**
         * Whether or not the current WME is the beta version.
         * @member {boolean} wLib.isBetaEditor
         */
        wLib.isBetaEditor = /beta/.test(location.href);
        
        /**
         * Sets window.wLib to the last wLib object and returns the current 
         * version.
         * @function wLib.noConflict
         */
        wLib.noConflict = function () {
            root.wLib = oldLib;
            return this;
        };
        
        /**
         * Namespace for functions related to geometry.
         * @namespace {Object} wLib.Geometry
         */
        wLib.Geometry = new Geometry;
        
        /**
         * Namespace for functions related to the model.
         * @namespace {Object} wLib.Model
         */
        wLib.Model = new Model;
        
        /**
         * Namespace for functions related to the WME interface
         * @namespace {Obect} wLib.Interface
         */
        wLib.Interface = new Interface;
        
        /**
         * Namespace for utility functions.
         * @namespace {Object} wLib.Util
         */
        wLib.Util = new Util;
        
        /**
         * Namespace for functions related to WME actions.
         * @namespace {Object} wLib.api
         */
        wLib.api = new API;

        /* Export library to window */
        root.wLib = wLib;
    }

    /*** GEOMETRY ***/
    function Geometry() {
        /**
         * Determines if an OpenLayers.Geometry is within the map view.
         * @function wLib.Geometry.isGeometryInMapExtent
         * @param geometry OpenLayers.Geometry
         * @return {Boolean} Whether or not the geometry is in the map extent.
         */
        this.isGeometryInMapExtent = function (geometry) {
            'use strict';
            return geometry && geometry.getBounds &&
                W.map.getExtent().intersectsBounds(geometry.getBounds());
        };
        
        /**
         * Determines if an {OpenLayers.LonLat} is within the map view.
         * @function wLib.Geometry.isLonLatInMapExtent
         * @param {OpenLayers.LonLat} lonLat The LonLat to check.
         * @return {Boolean} Whether or not the LonLat is in the map extent.
         */
        this.isLonLatInMapExtent = function (lonLat) {
            'use strict';
            return lonLat && W.map.getExtent().containsLonLat(lonLat);
        };
    };

    /*** MODEL ***/
    function Model() {
        /**
         * Returns whether or not the passed object or object id represents a 
         * venue.
         * @function wLib.Model.isPlace
         * @param {(String|Venue)} place A string representing the id of 
         * the place to test (only works if place is in the data extent) or the 
         * venue model object itself.
         * @return {Boolean} Whether or not the place is a venue.
         */
        this.isPlace = function (place) {
            if (typeof place === 'string') {
                place = W.model.venues.get(place);
            }
            return place && place.CLASS_NAME === 'Waze.Feature.Vector.Landmark';
        },
    
        /**
         * Returns the geometry object representing the navigation stop point 
         * of a venue.
         * @function wLib.Model.getPlaceNavStopPoint
         * @param {(String|Venue)} place A string representing the id of 
         * the place to test (only works if place is in the data extent) or the 
         * venue model object itself.
         * @return {OL.Geometry.Point} The geometry object representing the 
         * navigation stop point of a venue.
         */
        this.getPlaceNavStopPoint = function (place) {
            if (typeof place === 'string') {
                place = W.model.venues.get(place);
            }
            return place && place.isPoint() ? place.geometry : place.attributes.entryExitPoints[0].point;
        };
    
        /**
        * Gets the IDs of any selected segments.
        * @function wLib.Model.getSelectedSegmentIDs
        * @return {Array} Array containing the IDs of selected segments.
        */
        this.getSelectedSegmentIDs = function () {
            'use strict';
            var i, n, selectedItems, item, segments = [];
            if (!W.selectionManager.hasSelectedItems()) {
                return false;
            } else {
                selectedItems = W.selectionManager.selectedItems;
                for (i = 0, n = selectedItems.length; i < n; i++) {
                    item = selectedItems[i].model;
                    if ('segment' === item.type) {
                        segments.push(item.attributes.id);
                    }
                }
                return segments.length === 0 ? false : segments;
            }
        };

        /**
         * Defers execution of a callback function until the WME map and data 
         * model are ready. Call this function before calling a function that 
         * causes a map and model reload, such as W.map.moveTo(). After the 
         * move is completed the callback function will be executed.
         * @function wLib.Model.onModelReady
         * @param {Function} callback The callback function to be executed.
         * @param {Boolean} now Whether or not to call the callback now if the
         * model is currently ready.
         * @param {Object} context The context in which to call the callback.
         */
        this.onModelReady = function (callback, now, context) {
            var deferModelReady = function () {
                return $.Deferred(function (dfd) {
                    var resolve = function () {
                        dfd.resolve();
                        W.model.events.unregister('mergeend', null, resolve);
                    };
                    W.model.events.register('mergeend', null, resolve);
                }).promise();
            };
            var deferMapReady = function () {
                return $.Deferred(function (dfd) {
                    var resolve = function () {
                        dfd.resolve();
                        W.vent.off('operationDone', resolve);
                    };
                    W.vent.on('operationDone', resolve);
                }).promise();
            };

            if (typeof callback === 'function') {
                context = context || callback;
                if (now && wLib.Util.mapReady() && wLib.Util.modelReady()) {
                    callback.call(context);
                } else {
                    $.when(deferMapReady() && deferModelReady()).
                        then(function () {
                            callback.call(context);
                        });
                }
            }
        };
    
        /**
         * Retrives a route from the Waze Live Map.
         * @class
         * @name wLib.Model.RouteSelection
         * @param firstSegment The segment to use as the start of the route.
         * @param lastSegment The segment to use as the destination for the route.
         * @param {Array|Function} callback A function or array of funcitons to be 
         * executed after the route
         * is retrieved. 'This' in the callback functions will refer to the 
         * RouteSelection object.
         * @param {Object} options A hash of options for determining route. Valid 
         * options are:
         * fastest: {Boolean} Whether or not the fastest route should be used. 
         * Default is false, which selects the shortest route.
         * freeways: {Boolean} Whether or not to avoid freeways. Default is false.
         * dirt: {Boolean} Whether or not to avoid dirt roads. Default is false.
         * longtrails: {Boolean} Whether or not to avoid long dirt roads. Default 
         * is false.
         * uturns: {Boolean} Whether or not to allow U-turns. Default is true.
         * @return {wLib.Model.RouteSelection} The new RouteSelection object.
         * @example: // The following example will retrieve a route from the Live Map and select the segments in the route.
         * selection = W.selectionManager.selectedItems;
         * myRoute = new wLib.Model.RouteSelection(selection[0], selection[1], function(){this.selectRouteSegments();}, {fastest: true});
         */
        this.RouteSelection = function (firstSegment, lastSegment, callback, options) {
            var i,
                n,
                start = this.getSegmentCenterLonLat(firstSegment),
                end = this.getSegmentCenterLonLat(lastSegment);
            this.options = {
                fastest: options && options.fastest || false,
                freeways: options && options.freeways || false,
                dirt: options && options.dirt || false,
                longtrails: options && options.longtrails || false,
                uturns: options && options.uturns || true
            };
            this.requestData = {
                from: 'x:' + start.x + ' y:' + start.y + ' bd:true',
                to: 'x:' + end.x + ' y:' + end.y + ' bd:true',
                returnJSON: true,
                returnGeometries: true,
                returnInstructions: false,
                type: this.options.fastest ? 'HISTORIC_TIME' : 'DISTANCE',
                clientVersion: '4.0.0',
                timeout: 60000,
                nPaths: 3,
                options: this.setRequestOptions(this.options)
            };
            this.callbacks = [];
            if (callback) {
                if (!(callback instanceof Array)) {
                    callback = [callback];
                }
                for (i = 0, n = callback.length; i < n; i++) {
                    if ('function' === typeof callback[i]) {
                        this.callbacks.push(callback[i]);
                    }
                }
            }
            this.routeData = null;
            this.getRouteData();
        };

        this.RouteSelection.prototype = 
            /** @lends wLib.Model.RouteSelection.prototype */ {
                
            /**
             * Formats the routing options string for the ajax request.
             * @private
             * @param {Object} options Object containing the routing options.
             * @return {String} String containing routing options.
             */
            setRequestOptions: function (options) {
                return 'AVOID_TOLL_ROADS:' + (options.tolls ? 't' : 'f') + ',' +
                    'AVOID_PRIMARIES:' + (options.freeways ? 't' : 'f') + ',' +
                    'AVOID_TRAILS:' + (options.dirt ? 't' : 'f') + ',' +
                    'AVOID_LONG_TRAILS:' + (options.longtrails ? 't' : 'f') + ',' +
                    'ALLOW_UTURNS:' + (options.uturns ? 't' : 'f');
            },
            
            /**
             * Gets the center of a segment in LonLat form.
             * @private
             * @param segment A Waze model segment object.
             * @return {OpenLayers.LonLat} The LonLat object corresponding to the
             * center of the segment.
             */
            getSegmentCenterLonLat: function (segment) {
                var x, y, componentsLength, midPoint;
                if (segment) {
                    componentsLength = segment.geometry.components.length;
                    midPoint = Math.floor(componentsLength / 2);
                    if (componentsLength % 2 === 1) {
                        x = segment.geometry.components[midPoint].x;
                        y = segment.geometry.components[midPoint].y;
                    } else {
                        x = (segment.geometry.components[midPoint - 1].x +
                            segment.geometry.components[midPoint].x) / 2;
                        y = (segment.geometry.components[midPoint - 1].y +
                            segment.geometry.components[midPoint].y) / 2;
                    }
                    return new OL.Geometry.Point(x, y).
                        transform(W.map.getProjectionObject(), 'EPSG:4326');
                }

            },
            
            /**
             * Gets the route from Live Map and executes any callbacks upon success.
             * @private
             * @returns The ajax request object. The responseJSON property of the 
             * returned object
             * contains the route information.
             *
             */
            getRouteData: function () {
                var i,
                    n,
                    that = this;
                return $.ajax({
                    dataType: 'json',
                    url: this.getURL(),
                    data: this.requestData,
                    dataFilter: function (data, dataType) {
                        return data.replace(/NaN/g, '0');
                    },
                    success: function (data) {
                        that.routeData = data;
                        for (i = 0, n = that.callbacks.length; i < n; i++) {
                            that.callbacks[i].call(that);
                        }
                    }
                });
            },
            
            /**
             * Extracts the IDs from all segments on the route.
             * @private
             * @return {Array} Array containing an array of segment IDs for
             * each route alternative.
             */
            getRouteSegmentIDs: function () {
                var i, j, route, len1, len2, segIDs = [],
                    routeArray = [],
                    data = this.routeData;
                if ('undefined' !== typeof data.alternatives) {
                    for (i = 0, len1 = data.alternatives.length; i < len1; i++) {
                        route = data.alternatives[i].response.results;
                        for (j = 0, len2 = route.length; j < len2; j++) {
                            routeArray.push(route[j].path.segmentId);
                        }
                        segIDs.push(routeArray);
                        routeArray = [];
                    }
                } else {
                    route = data.response.results;
                    for (i = 0, len1 = route.length; i < len1; i++) {
                        routeArray.push(route[i].path.segmentId);
                    }
                    segIDs.push(routeArray);
                }
                return segIDs;
            },
            
            /**
             * Gets the URL to use for the ajax request based on country.
             * @private
             * @return {String} Relative URl to use for route ajax request.
             */
            getURL: function () {
                if (W.model.countries.get(235) || W.model.countries.get(40)) {
                    return '/RoutingManager/routingRequest';
                } else if (W.model.countries.get(106)) {
                    return '/il-RoutingManager/routingRequest';
                } else {
                    return '/row-RoutingManager/routingRequest';
                }
            },
            
            /**
             * Selects all segments on the route in the editor.
             * @param {Integer} routeIndex The index of the alternate route.
             * Default route to use is the first one, which is 0.
             */
            selectRouteSegments: function (routeIndex) {
                var i, n, seg,
                    segIDs = this.getRouteSegmentIDs()[Math.floor(routeIndex) || 0],
                    segments = [];
                if ('undefined' === typeof segIDs) {
                    return;
                }
                for (i = 0, n = segIDs.length; i < n; i++) {
                    seg = W.model.segments.get(segIDs[i]);
                    if ('undefined' !== seg) {
                        segments.push(seg);
                    }
                }
                return W.selectionManager.select(segments);
            }
        };
    };

    /*** INTERFACE ***/
    function Interface() {
        /**
         * Generates id for message bars.
         * @private
         */
        var getNextID = function () {
            var id = 1;
            return function () {
                return id++;
            };
        } ();

        this.MessageBar = OL.Class(this, /** @lends wLib.Interface.MessageBar.prototype */ {
            $el: null,
            id: null,
            elementID: null,
            divStyle: {
                'margin': 'auto',
                'border-radius': '10px',
                'text-align': 'center',
                'width': '40%',
                'font-size': '1em',
                'font-weight': 'bold',
                'color': 'white'
            },
            
            /**
             * Class to store individual message information.
             * @class {Backbone.Model} Message
             * @private 
             */
            Message: Backbone.Model.extend({
                defaults: {
                    messageName: null,
                    messageType: 'info',
                    messageText: '',
                    displayDuration: null,
                    skipPrefix: false
                }
            }),
            
            /**
             * Class to display messages on page.
             * @class {Object} MessageView
             * @private 
             */
            MessageView: Backbone.View.extend({
                styles: {
                    defaultStyle: {
                        'border-radius': '20px',
                        'display': 'inline-block',
                        'padding': '5px',
                        'background-color': 'rgba(0,0,0,0.7)'
                    },
                    error: {
                        'background-color': 'rgba(180,0,0,0.9)',
                        'color': 'black'
                    },
                    warn: {
                        'background-color': 'rgba(230,230,0,0.9)',
                        'color': 'black'
                    },
                    info: {
                        'background-color': 'rgba(0,0,230,0.9)'
                    }
                },
                template: function () {
                    var messageText = '',
                        style,
                        $messageEl = $('<p/>');

                    if (!this.model.attributes.skipPrefix && this.messagePrefix) {
                        messageText = this.messagePrefix + ' ';
                    }

                    messageText += this.model.attributes.messageText;

                    style = (this.model.attributes.messageType &&
                        this.styles[this.model.attributes.messageType]) ?
                        this.styles[this.model.attributes.messageType] : this.styles.defaultStyle;
                    style = _.defaults(style, this.styles.defaultStyle);

                    $messageEl.
                        css(style).
                        text(messageText);

                    return $messageEl;
                },
                initialize: function () {
                    this.render();
                },
                render: function () {
                    this.$el.
                        append(this.template()).
                        appendTo(this.messageBar.$el).
                        fadeIn('fast').
                        delay(this.model.attributes.displayDuration ||
                            this.displayDuration || 5000).
                        fadeOut('slow', function () {
                            $(this).remove();
                        });
                    return this;
                }
            }),
            
            /**
             * Class to hold Messages.
             * @class {Object} MessageCollection
             * @private 
             */
            MessageCollection: Backbone.Collection.extend(),

            messages: null,
            
            /**
             * An object containing message attributes.
             * @typedef {Object} wLib.Interface.MessageBar.messageAttributes
             * @property {string} [messageName] The name of the message, used for 
             * looking up the message.
             * @property {string} [messageType] The name of the type/style of 
             * the message, used for looking up the message display css options. 
             * Default styles include 'info', 'warn', and 'error'. You can also use 
             * a custom-defined messageType (see {@link 
             * wLib.Interface.MessageBar.addMessageType}).
             * @property {string} messageText The text to display when showing the 
             * message.
             * @property {number} [displayDuration] The duration to display the 
             * message in milliseconds.
             * @property {boolean} [skipPrefix] Whether or not to skip the default 
             * message prefix when displaying the message.
             */
        
            /**
             * @typedef {object} wLib.Interface.MessageBar.options
             * @property {array<wLib.Interface.MessageBar.messageAttributes>} 
             * [messages] An array of objects containing message attributes to save 
             * during initialization.
             * @property {string} [messagePrefix] The prefix to prepend to each 
             * message. This can be disabled per message by using skipPrefix in the 
             * message attributes.
             * @property {number} [displayDuration] The duration to display message 
             * by default in milliseconds.
             * @property {object} [styles] Hash with keys representing a name for 
             * the style/messageType and values containing objects with css 
             * properties that style/messageType.
             */
        
            /**
             * Creates a new message bar.
             * @class
             * @name wLib.Interface.MessageBar
             * @param [options] {wLib.Interface.MessageBar.options} Options to set 
             * during initialization
             * @example myMessageBar = new wLib.Interface.MessageBar({
             *  messages: [{
             *      messageName: 'exampleMessage',
             *      messageType: 'info',
             *      displayDuration: 5000,
             *      skipPrefix: true
             *  }],
             *  displayDuration: 10000,
             *  messagePrefix: 'My Script:',
             *  styles: {
             *      'purpleBackground': {
             *          'background-color': 'rgba(255,0,255,0.9)'
             *      }
             *  }
             * }); 
             */
            initialize: function (options) {
                var $insertTarget = $('#search');

                options = _.defaults(options || {}, {
                    messagePrefix: null,
                    messages: [],
                    styles: {},
                    displayDuration: 5000
                });

                this.messages = new this.MessageCollection({ model: this.Message });
                this.id = getNextID();
                this.elementID = 'wlib-messagebar-' + this.id;

                _(options.styles).each(function (style, name) {
                    this.addMessageType(name, style);
                }, this);

                _(options.messages).each(function (message) {
                    this.messages.add(message);
                }, this);

                this.MessageView.prototype.messagePrefix = options.messagePrefix;
                this.MessageView.prototype.displayDuration = options.displayDuration;
                this.MessageView.prototype.messageBar = this;

                this.$el = $('<div/>').
                    css(this.divStyle).
                    attr('id', this.elementID);

                wLib.Util.waitForElement($insertTarget, function () {
                    this.$el.insertAfter($insertTarget);
                }, this);
            },
            
            /**
             * Adds a style for a message type.
             * @param {String} name The name of the message type/style.
             * @param style {Object} Object containing css properties and 
             * values to use for the new messageType.
             */
            addMessageType: function (name, style) {
                style = style || {};

                if (name) {
                    this.MessageView.prototype.styles[name] = style;
                }

                return this;
            },
            
            /**
             * Removes the message bar from the page.
             */
            destroy: function () {
                this.messages.reset();
                this.$el.remove();
            },
            
            /**
             * Displays a message.
             * @param {(string|wLib.Interface.MessageBar.messageAttributes)} message A hash with message options for a new message or the name of a saved message to look up.
             * @example myMessageBar.displayMessage('previouslysavedmessage');
             * @example myMessageBar.displayMessage({
             *      messageName: 'newMessage',
             *      messageType: 'warn',
             *      displayDuration: 5000,
             *      skipPrefix: true
             *  });
             */
            displayMessage: function (message) {
                if (typeof message === 'string') {
                    // look up message by name and display
                    message = this.messages.findWhere({ 'messageName': message });
                } else {
                    // add the new message object to the collection and display
                    message = this.messages.add(message);
                }
                new this.MessageView({
                    model: message
                });
                if (!message.attributes.messageName) {
                    this.messages.remove(message);
                }
            },
            
            /**
             * Adds a new message to the collection of saved messages.
             * @param {wLib.Interface.MessageBar.messageAttributes} messageObject 
             * The attributes to use for the new message.
             * @example myMessageBar.saveMessage({
             *      messageName: 'exampleMessage',
             *      messageType: 'info',
             *      displayDuration: 5000,
             *      skipPrefix: true
             * });
             */
            saveMessage: function (messageObject) {
                this.messages.add(messageObject);
                return this;
            }
        });

        this.Options = OL.Class(this,
        /** @lends wLib.Interface.Options.prototype */ {
                localStorageName: null,
                options: {},
                
                /**
                 * Creates a new Options object to handle saving and retrieving 
                 * script options. During initialization, any options stored under 
                 * the named key in localStorage will be loaded. Any options 
                 * provided as a parameter to the constructor will be applied to 
                 * the retrieved options and thus may overwrite any stored values.
                 * @name wLib.Interface.Options
                 * @class
                 * @param {String} name The string used as the localStorage key 
                 * under which to store the options.
                 * @param {Object} options A hash containing options to set during 
                 * initialization.
                 * @example var myOptions = new wLib.Interface.Options('thebestscriptever', {scriptVersion: x});
                 * myOptions.set('option1', true);
                 * myOptions.set({'option2': false, 'option3': 'very true'});
                 * myOptions.get('option2') === false // true;
                 */
                initialize: function (name, options) {
                    var i = 1;

                    if (window.localStorage && typeof name === 'string') {

                        this.localStorageName = name.toLowerCase().
                            replace(/[^a-z]/g, '');

                        if (localStorage.getItem(this.localStorageName)) {
                            while (localStorage.getItem(
                                this.localStorageName + i)) {
                                i += 1;
                            }
                            this.localStorageName = this.localStorageName + i;
                        }

                        this.retrieveOptions();

                        if (options && _.isObject(options)) {
                            this.set(options);
                        }
                    }
                },
                
                /**
                 * Clears all stored options.
                 */
                clear: function () {
                    this.options = null;
                    this.saveOptions();
                    return this;
                },
                
                /**
                 * Retrieves a stored value. If no key is specified, the entire 
                 * options object is returned.
                 * @param {String} key Optional. The key to retrieve.
                 */
                get: function (key) {
                    return key && this.options[key] || this.options;
                },
                
                /**
                 * Saves the options to localStorage
                 * @private
                 */
                saveOptions: function () {
                    localStorage[this.localStorageName] =
                    JSON.stringify(this.options);
                },
                
                /**
                 * Stores a value under the provided key. Provide either an object 
                 * hash of keys and values to store as a single parameter or 
                 * provide a key and value as two parameters.
                 * @param {Object} key The name of the option. Can be string, 
                 * number if providing a value as the second parameter, or a hash 
                 * of multiple options (see function description).
                 * @param {Any} value The value to store. Not used if providing a 
                 * hash as the first argument.
                 * @example myOptions.set('option1', true); // or
                 * myOptions.set({'option2': false, 'option3': 'very true'});
                 */
                set: function (key, value) {
                    var j;
                    if ((typeof key === 'string' || !isNaN(key)) && value) {
                        this.options[key] = value;
                    } else if (_.isObject(key)) {
                        for (j in key) {
                            if (key.hasOwnProperty(j)) {
                                this.options[j] = key[j];
                            }
                        }
                    }
                    this.saveOptions();
                    return this;
                }, 
                          
                /**
                 * Retrieves options previously stored in localStorage.
                 * @private
                 */
                retrieveOptions: function () {
                    var options = localStorage[this.localStorageName];
                    if (options) {
                        this.options = options;
                    }
                }
            });

        this.Shortcut = OL.Class(this, /** @lends wLib.Interface.Shortcut.prototype */ {
            name: null,
            group: null,
            shortcut: {},
            callback: null,
            scope: null,
            groupExists: false,
            actionExists: false,
            eventExists: false,
            defaults: {
                group: 'default'
            },
                
            /**
             * Creates a new {wLib.Interface.Shortcut}.
             * @class
             * @name wLib.Interface.Shortcut
             * @param name {String} The name of the shortcut.
             * @param group {String} The name of the shortcut group.
             * @param shortcut {String} The shortcut key(s). The shortcut  
             * should be of the form 'i' where i is the keyboard shortuct or 
             * include modifier keys  such as 'CSA+i', where C = the control 
             * key, S = the shift key, A = the alt key, and i = the desired 
             * keyboard shortcut. The modifier keys are optional.
             * @param callback {Function} The function to be called by the 
             * shortcut.
             * @param scope {Object} The object to be used as this by the 
             * callback.
             * @return {wLib.Interface.Shortcut} The new shortcut object.
             * @example //Creates new shortcut and adds it to the map.
             * shortcut = new wLib.Interface.Shortcut('myName', 'myGroup', 'C+p', callbackFunc, null).add();
             */
            initialize: function (name, group, shortcut, callback, scope) {
                if ('string' === typeof name && name.length > 0 &&
                    'string' === typeof shortcut && shortcut.length > 0 &&
                    'function' === typeof callback) {
                    this.name = name;
                    this.group = group || this.defaults.group;
                    this.callback = callback;
                    this.shortcut[shortcut] = name;
                    if ('object' !== typeof scope) {
                        this.scope = null;
                    } else {
                        this.scope = scope;
                    }
                    return this;
                }
            },
                
            /**
            * Determines if the shortcut's group already exists.
            * @private
            */
            doesGroupExist: function () {
                this.groupExists = 'undefined' !== typeof W.accelerators.Groups[this.group] &&
                undefined !== typeof W.accelerators.Groups[this.group].members &&
                W.accelerators.Groups[this.group].length > 0;
                return this.groupExists;
            },
                
            /**
            * Determines if the shortcut's action already exists.
            * @private
            */
            doesActionExist: function () {
                this.actionExists = 'undefined' !== typeof W.accelerators.Actions[this.name];
                return this.actionExists;
            },
                
            /**
            * Determines if the shortcut's event already exists.
            * @private
            */
            doesEventExist: function () {
                this.eventExists = 'undefined' !== typeof W.accelerators.events.listeners[this.name] &&
                W.accelerators.events.listeners[this.name].length > 0 &&
                this.callback === W.accelerators.events.listeners[this.name][0].func &&
                this.scope === W.accelerators.events.listeners[this.name][0].obj;
                return this.eventExists;
            },
                
            /**
            * Creates the shortcut's group.
            * @private
            */
            createGroup: function () {
                W.accelerators.Groups[this.group] = [];
                W.accelerators.Groups[this.group].members = [];
            },
                
            /**
            * Registers the shortcut's action.
            * @private
            */
            addAction: function () {
                W.accelerators.addAction(this.name, { group: this.group });
            },
                
            /**
            * Registers the shortcut's event.
            * @private
            */
            addEvent: function () {
                W.accelerators.events.register(this.name, this.scope, this.callback);
            },
                
            /**
            * Registers the shortcut's keyboard shortcut.
            * @private
            */
            registerShortcut: function () {
                W.accelerators._registerShortcuts(this.shortcut);
            },
                
            /**
            * Adds the keyboard shortcut to the map.
            * @return {wLib.Interface.Shortcut} The keyboard shortcut.
            */
            add: function () {
                /* If the group is not already defined, initialize the group. */
                if (!this.doesGroupExist()) {
                    this.createGroup();
                }

                /* Clear existing actions with same name */
                if (this.doesActionExist()) {
                    W.accelerators.Actions[this.name] = null;
                }
                this.addAction();

                /* Register event only if it's not already registered */
                if (!this.doesEventExist()) {
                    this.addEvent();
                }

                /* Finally, register the shortcut. */
                this.registerShortcut();
                return this;
            },
                
            /**
            * Removes the keyboard shortcut from the map.
            * @return {wLib.Interface.Shortcut} The keyboard shortcut.
            */
            remove: function () {
                if (this.doesEventExist()) {
                    W.accelerators.events.unregister(this.name, this.scope, this.callback);
                }
                if (this.doesActionExist()) {
                    delete W.accelerators.Actions[this.name];
                }
                //remove shortcut?
                return this;
            },
                
            /**
            * Changes the keyboard shortcut and applies changes to the map.
            * @return {wLib.Interface.Shortcut} The keyboard shortcut.
            */
            change: function (shortcut) {
                if (shortcut) {
                    this.shortcut = {};
                    this.shortcut[shortcut] = this.name;
                    this.registerShortcut();
                }
                return this;
            }
        }),

        this.Tab = OL.Class(this, {
            /** @lends wLib.Interface.Tab */
            TAB_SELECTOR: '#user-tabs ul.nav-tabs',
            CONTENT_SELECTOR: '#user-info div.tab-content',
            callback: null,
            $content: null,
            context: null,
            $tab: null,
            
            /**
             * Creates a new wLib.Interface.Tab. The tab is appended to the WME 
             * editor sidebar and contains the passed HTML content.
             * @class
             * @name wLib.Interface.Tab
             * @param {String} name The name of the tab. Should not contain any 
             * special characters.
             * @param {String} content The HTML content of the tab.
             * @param {Function} [callback] A function to call upon successfully 
             * appending the tab.
             * @param {Object} [context] The context in which to call the callback 
             * function.
                     * @return {wLib.Interface.Tab} The new tab object.
             * @example //Creates new tab and adds it to the page.
             * new wLib.Interface.Tab('thebestscriptever', '<div>Hello World!</div>');
             */
            initialize: function (name, content, callback, context) {
                var idName, i = 0;
                if (name && 'string' === typeof name &&
                    content && 'string' === typeof content) {
                    if (callback && 'function' === typeof callback) {
                        this.callback = callback;
                        this.context = context || callback;
                    }
                    /* Sanitize name for html id attribute */
                    idName = name.toLowerCase().replace(/[^a-z-_]/g, '');
                    /* Make sure id will be unique on page */
                    while (
                        $('#sidepanel-' + (i ? idName + i : idName)).length > 0) {
                        i++;
                    }
                    if (i) {
                        idName = idName + i;
                    }
                    /* Create tab and content */
                    this.$tab = $('<li/>')
                        .append($('<a/>')
                            .attr({
                                'href': '#sidepanel-' + idName,
                                'data-toggle': 'tab',
                            })
                            .text(name));
                    this.$content = $('<div/>')
                        .addClass('tab-pane')
                        .attr('id', 'sidepanel-' + idName)
                        .html(content);

                    this.appendTab();
                }
            },

            append: function (content) {
                this.$content.append(content);
            },

            appendTab: function () {
                wLib.Util.waitForElement(
                    this.TAB_SELECTOR + ',' + this.CONTENT_SELECTOR,
                    function () {
                        $(this.TAB_SELECTOR).append(this.$tab);
                        $(this.CONTENT_SELECTOR).first().append(this.$content);
                        if (this.callback) {
                            this.callback.call(this.context);
                        }
                    }, this);
            },

            clearContent: function () {
                this.$content.empty();
            },

            destroy: function () {
                this.$tab.remove();
                this.$content.remove();
            }
        });
    };

    /*** Utilities ***/
    function Util() {
        /**
         * Function to track the ready state of the map.
         * @function wLib.Util.mapReady
         * @return {Boolean} Whether or not a map operation is pending or 
         * undefined if the function has not yet seen a map ready event fired.
         */
        this.mapReady = function () {
            var mapReady = true;
            W.vent.on('operationPending', function () {
                mapReady = false;
            });
            W.vent.on('operationDone', function () {
                mapReady = true;
            });
            return function () {
                return mapReady;
            };
        } ();

        /**
         * Function to track the ready state of the model.
         * @function wLib.Util.modelReady
         * @return {Boolean} Whether or not the model has loaded objects or 
         * undefined if the function has not yet seen a model ready event fired.
         */
        this.modelReady = function () {
            var modelReady = true;
            W.model.events.register('mergestart', null, function () {
                modelReady = false;
            });
            W.model.events.register('mergeend', null, function () {
                modelReady = true;
            });
            return function () {
                return modelReady;
            };
        } ();
    
        /**
         * Function to defer function execution until an element is present on 
         * the page.
         * @function wLib.Util.waitForElement
         * @param {String} selector The CSS selector string or a jQuery object 
         * to find before executing the callback.
         * @param {Function} callback The function to call when the page 
         * element is detected.
         * @param {Object} [context] The context in which to call the callback.
         */
        this.waitForElement = function (selector, callback, context) {
            var jqObj;

            if (!selector || typeof callback !== 'function') {
                return;
            }

            jqObj = typeof selector === 'string' ?
                $(selector) : selector instanceof $ ? selector : null;

            if (!jqObj.size()) {
                window.requestAnimationFrame(function () {
                    wLib.Util.waitForElement(selector, callback, context);
                });
            } else {
                callback.call(context || callback);
            }
        };
    
        /**
         * Returns a callback function in the appropriate scope.
         * @function wLib.Util.createCallback
         * @private
         * @param {function} func The callback function.
         * @param {object} [scope] The scope in which to call the callback.
         * @return {function} A function that returns the callback function 
         * called in the appropriate scope.
         */
        this.createCallback = function (func, scope) {
            return typeof func === 'function' && function () {
                return func.call(scope || func);
            };
        };

    };

    /*** API ***/
    function API() {

        this.UpdateFeatureAddress = function () {
            return require('Waze/Action/UpdateFeatureAddress');
        } ();

        this.UpdateFeatureGeometry = function () {
            return require('Waze/Action/UpdateFeatureGeometry');
        } ();

        this.UpdateObject = function () {
            return require('Waze/Action/UpdateObject');
        } ();

        this.UpdateSegmentGeometry = function () {
            return require('Waze/Action/UpdateSegmentGeometry');
        } ();
    
        /**
         * Updates the address for a place (venue).
         * @function wLib.api.updateFeatureAddress
         * @param {(String|Venue)} place A string containing the id of 
         * the venue to change or the venue model object itself.
         * @param address {Object} An object containing the country, state, city, 
         * and street objects to use as the new address.
         * objects.
         */
        this.updateFeatureAddress = function (place, address) {
            'use strict';
            var newAttributes,
                UpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress');
            if (typeof place === 'string') {
                place = W.model.venues.get(place);
            }
            if (place && place.CLASS_NAME === 'Waze.Feature.Vector.Landmark' &&
                address && address.state && address.country) {
                newAttributes = {
                    countryID: address.country.id,
                    stateID: address.state.id,
                    cityName: address.city.name,
                    emptyCity: address.city.name ? null : true,
                    streetName: address.street.name,
                    emptyStreet: address.street.name ? null : true
                };
                W.model.actionManager.add(
                    new UpdateFeatureAddress(place, newAttributes));
            }
        };
    
        /**
         * Updates the geometry of a place (venue) in WME.
         * @function wLib.api.updateFeatureGeometry
         * @param {(String|Venue)} place A string containing the id of 
         * the venue to change or the venue model object itself.
         * @param {(OL.Geometry.Point|OL.Geometry.Polygon)} newGeometry The 
         * geometry object to use as the new geometry for the venue.
         */
        this.updateFeatureGeometry = function (place, newGeometry) {
            var oldGeometry,
                model = W.model.venues,
                WMEUPdateFeatureGeometry = require('Waze/Action/UpdateFeatureGeometry');
            if (typeof place === 'string') {
                place = W.model.venues.get(place);
            }
            if (place && place.CLASS_NAME === 'Waze.Feature.Vector.Landmark' &&
                newGeometry && (newGeometry instanceof OL.Geometry.Point ||
                    newGeometry instanceof OL.Geometry.Polygon)) {
                oldGeometry = place.attributes.geometry.clone();
                W.model.actionManager.add(
                    new WMEUPdateFeatureGeometry(place, model, oldGeometry,
                        newGeometry));
            }
        };
    };
}.call(this));