<commit_before>/*
Copyright 2014 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

/*
This service handles what should happen when you get an event. This service does
not care where the event came from, it only needs enough context to be able to
process them. Events may be coming from the event stream, the REST API (via
direct GETs or via a pagination stream API), etc.

Typically, this service will store events or broadcast them to any listeners
(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
    var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
    var MSG_EVENT = "MSG_EVENT";
    var MEMBER_EVENT = "MEMBER_EVENT";
    var PRESENCE_EVENT = "PRESENCE_EVENT";
    var POWERLEVEL_EVENT = "POWERLEVEL_EVENT";
    var CALL_EVENT = "CALL_EVENT";
    var NAME_EVENT = "NAME_EVENT";

    var initialSyncDeferred = $q.defer();

    $rootScope.events = {
        rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
    };

    // used for dedupping events - could be expanded in future...
    // FIXME: means that we leak memory over time (along with lots of the rest
    // of the app, given we never try to reap memory yet)
    var eventMap = {};

    $rootScope.presence = {};

    var initRoom = function(room_id) {
        if (!(room_id in $rootScope.events.rooms)) {
            console.log("Creating new handler entry for " + room_id);
            $rootScope.events.rooms[room_id] = {};
            $rootScope.events.rooms[room_id].messages = [];
            $rootScope.events.rooms[room_id].members = {};

            // Pagination information
            $rootScope.events.rooms[room_id].pagination = {
                earliest_token: "END"   // how far back we've paginated
            }
        }
    };

    var resetRoomMessages = function(room_id) {
        if ($rootScope.events.rooms[room_id]) {
            $rootScope.events.rooms[room_id].messages = [];
        }
    };

    var handleRoomCreate = function(event, isLiveEvent) {
        initRoom(event.room_id);

        // For now, we do not use the event data. Simply signal it to the app controllers
        $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
    };

    var handleRoomAliases = function(event, isLiveEvent) {
        matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
    };

    var handleMessage = function(event, isLiveEvent) {
        initRoom(event.room_id);

        if (isLiveEvent) {
            if (event.user_id === matrixService.config().user_id &&
                (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
                // Assume we've already echoed it. So, there is a fake event in the messages list of the room
                // Replace this fake event by the true one
                var index = getRoomEventIndex(event.room_id, event.event_id);
                if (index) {
                    $rootScope.events.rooms[event.room_id].messages[index] = event;
                }
                else {
                    $rootScope.events.rooms[event.room_id].messages.push(event);
                }
            }
            else {
                $rootScope.events.rooms[event.room_id].messages.push(event);
            }
        }
        else {
            $rootScope.events.rooms[event.room_id].messages.unshift(event);
        }

        // TODO send delivery receipt if isLiveEvent

        // $broadcast this, as controllers may want to do funky things such as
        // scroll to the bottom, etc which cannot be expressed via simple $scope
        // updates.
        $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
    };

    var handleRoomMember = function(event, isLiveEvent, isStateEvent) {
        initRoom(event.room_id);

        // if the server is stupidly re-relaying a no-op join, discard it.
        if (event.prev_content &&
            event.content.membership === "join" &&
            event.content.membership === event.prev_content.membership)
        {
            return;
        }

        // add membership changes as if they were a room message if something interesting changed
        // Exception: Do not do this if the event is a room state event because such events already come
        // as room messages events. Moreover, when they come as room messages events, they are relatively ordered
        // with other other room messages
        if (event.content.prev !== event.content.membership && !isStateEvent) {
            if (isLiveEvent) {
                $rootScope.events.rooms[event.room_id].messages.push(event);
            }
            else {
                $rootScope.events.rooms[event.room_id].messages.unshift(event);
            }
        }

        // Use data from state event or the latest data from the stream.
        // Do not care of events that come when paginating back
        if (isStateEvent || isLiveEvent) {
            $rootScope.events.rooms[event.room_id].members[event.state_key] = event;
        }

        $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent);
    };

    var handlePresence = function(event, isLiveEvent) {
        $rootScope.presence[event.content.user_id] = event;
        $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
    };

    var handlePowerLevels = function(event, isLiveEvent) {
        initRoom(event.room_id);

        // Keep the latest data. Do not care of events that come when paginating back
        if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) {
            $rootScope.events.rooms[event.room_id][event.type] = event;
            $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);
        }
    };

    var handleRoomName = function(event, isLiveEvent) {
        console.log("handleRoomName " + isLiveEvent);

        initRoom(event.room_id);

        $rootScope.events.rooms[event.room_id][event.type] = event;
        $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
    };

    // TODO: Can this just be a generic "I am a room state event, can haz store?"
    var handleRoomTopic = function(event, isLiveEvent) {
        console.log("handleRoomTopic live="+isLiveEvent);

        initRoom(event.room_id);

        // live events always update, but non-live events only update if the
        // ts is later.
        if (!isLiveEvent) {
            var eventTs = event.ts;
            var storedEvent = $rootScope.events.rooms[event.room_id][event.type];
            if (storedEvent) {
                if (storedEvent.ts > eventTs) {
                    // ignore it, we have a newer one already.
                    return;
                }
            }
        }
        $rootScope.events.rooms[event.room_id][event.type] = event;
    };

    var handleCallEvent = function(event, isLiveEvent) {
        $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
        if (event.type == 'm.call.invite') {
            $rootScope.events.rooms[event.room_id].messages.push(event);
        }
    };

    /**
     * Get the index of the event in $rootScope.events.rooms[room_id].messages
     * @param {type} room_id the room id
     * @param {type} event_id the event id to look for
     * @returns {Number | undefined} the index. undefined if not found.
     */
    var getRoomEventIndex = function(room_id, event_id) {
        var index;

        var room = $rootScope.events.rooms[room_id];
        if (room) {
            for (var i = 0; i < room.messages.length; i++) {
                var message = room.messages[i];
                console.log(message.event_id);
                if (event_id === message.event_id) {
                    index = i;
                    break;
                }
            }
        }
        return index;
    }

    return {
        ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
        MSG_EVENT: MSG_EVENT,
        MEMBER_EVENT: MEMBER_EVENT,
        PRESENCE_EVENT: PRESENCE_EVENT,
        POWERLEVEL_EVENT: POWERLEVEL_EVENT,
        CALL_EVENT: CALL_EVENT,
        NAME_EVENT: NAME_EVENT,

        handleEvent: function(event, isLiveEvent, isStateEvent) {
            // Avoid duplicated events
            // Needed for rooms where initialSync has not been done.
            // In this case, we do not know where to start pagination. So, it starts from the END
            // and we can have the same event (ex: joined, invitation) coming from the pagination
            // AND from the event stream.
            // FIXME: This workaround should be no more required when /initialSync on a particular room
            // will be available (as opposite to the global /initialSync done at startup)
            if (!isStateEvent) {    // Do not consider state events
                if (event.event_id && eventMap[event.event_id]) {
                    console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4));
                    return;
                }
                else {
                    eventMap[event.event_id] = 1;
                }
            }

            if (event.type.indexOf('m.call.') === 0) {
                handleCallEvent(event, isLiveEvent);
            }
            else {
                switch(event.type) {
                    case "m.room.create":
                        handleRoomCreate(event, isLiveEvent);
                        break;
                    case "m.room.aliases":
                        handleRoomAliases(event, isLiveEvent);
                        break;
                    case "m.room.message":
                        handleMessage(event, isLiveEvent);
                        break;
                    case "m.room.member":
                        handleRoomMember(event, isLiveEvent, isStateEvent);
                        break;
                    case "m.presence":
                        handlePresence(event, isLiveEvent);
                        break;
                    case 'm.room.ops_levels':
                    case 'm.room.send_event_level':
                    case 'm.room.add_state_level':
                    case 'm.room.join_rules':
                    case 'm.room.power_levels':
                        handlePowerLevels(event, isLiveEvent);
                        break;
                    case 'm.room.name':
                        handleRoomName(event, isLiveEvent);
                        break;
                    case 'm.room.topic':
                        handleRoomTopic(event, isLiveEvent);
                        break;
                    default:
                        console.log("Unable to handle event type " + event.type);
                        console.log(JSON.stringify(event, undefined, 4));
                        break;
                }
            }
        },

        // isLiveEvents determines whether notifications should be shown, whether
        // messages get appended to the start/end of lists, etc.
        handleEvents: function(events, isLiveEvents, isStateEvents) {
            for (var i=0; i<events.length; i++) {
                this.handleEvent(events[i], isLiveEvents, isStateEvents);
            }
        },

        // Handle messages from /initialSync or /messages
        handleRoomMessages: function(room_id, messages, isLiveEvents) {
            this.handleEvents(messages.chunk, isLiveEvents);

            // Store how far back we've paginated
            // This assumes the paginations requests are contiguous and in reverse chronological order
            $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
        },

        handleInitialSyncDone: function(initialSyncData) {
            console.log("# handleInitialSyncDone");
            initialSyncDeferred.resolve(initialSyncData);
        },

        // Returns a promise that resolves when the initialSync request has been processed
        waitForInitialSyncCompletion: function() {
            return initialSyncDeferred.promise;
        },

        resetRoomMessages: function(room_id) {
            resetRoomMessages(room_id);
        }
    };
}]);
<commit_msg>getRoomEventIndex: improved speed for what it is used<commit_after>/*
Copyright 2014 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

/*
This service handles what should happen when you get an event. This service does
not care where the event came from, it only needs enough context to be able to
process them. Events may be coming from the event stream, the REST API (via
direct GETs or via a pagination stream API), etc.

Typically, this service will store events or broadcast them to any listeners
(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
    var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
    var MSG_EVENT = "MSG_EVENT";
    var MEMBER_EVENT = "MEMBER_EVENT";
    var PRESENCE_EVENT = "PRESENCE_EVENT";
    var POWERLEVEL_EVENT = "POWERLEVEL_EVENT";
    var CALL_EVENT = "CALL_EVENT";
    var NAME_EVENT = "NAME_EVENT";

    var initialSyncDeferred = $q.defer();

    $rootScope.events = {
        rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
    };

    // used for dedupping events - could be expanded in future...
    // FIXME: means that we leak memory over time (along with lots of the rest
    // of the app, given we never try to reap memory yet)
    var eventMap = {};

    $rootScope.presence = {};

    var initRoom = function(room_id) {
        if (!(room_id in $rootScope.events.rooms)) {
            console.log("Creating new handler entry for " + room_id);
            $rootScope.events.rooms[room_id] = {};
            $rootScope.events.rooms[room_id].messages = [];
            $rootScope.events.rooms[room_id].members = {};

            // Pagination information
            $rootScope.events.rooms[room_id].pagination = {
                earliest_token: "END"   // how far back we've paginated
            }
        }
    };

    var resetRoomMessages = function(room_id) {
        if ($rootScope.events.rooms[room_id]) {
            $rootScope.events.rooms[room_id].messages = [];
        }
    };

    var handleRoomCreate = function(event, isLiveEvent) {
        initRoom(event.room_id);

        // For now, we do not use the event data. Simply signal it to the app controllers
        $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
    };

    var handleRoomAliases = function(event, isLiveEvent) {
        matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
    };

    var handleMessage = function(event, isLiveEvent) {
        initRoom(event.room_id);

        if (isLiveEvent) {
            if (event.user_id === matrixService.config().user_id &&
                (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
                // Assume we've already echoed it. So, there is a fake event in the messages list of the room
                // Replace this fake event by the true one
                var index = getRoomEventIndex(event.room_id, event.event_id);
                if (index) {
                    $rootScope.events.rooms[event.room_id].messages[index] = event;
                }
                else {
                    $rootScope.events.rooms[event.room_id].messages.push(event);
                }
            }
            else {
                $rootScope.events.rooms[event.room_id].messages.push(event);
            }
        }
        else {
            $rootScope.events.rooms[event.room_id].messages.unshift(event);
        }

        // TODO send delivery receipt if isLiveEvent

        // $broadcast this, as controllers may want to do funky things such as
        // scroll to the bottom, etc which cannot be expressed via simple $scope
        // updates.
        $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
    };

    var handleRoomMember = function(event, isLiveEvent, isStateEvent) {
        initRoom(event.room_id);

        // if the server is stupidly re-relaying a no-op join, discard it.
        if (event.prev_content &&
            event.content.membership === "join" &&
            event.content.membership === event.prev_content.membership)
        {
            return;
        }

        // add membership changes as if they were a room message if something interesting changed
        // Exception: Do not do this if the event is a room state event because such events already come
        // as room messages events. Moreover, when they come as room messages events, they are relatively ordered
        // with other other room messages
        if (event.content.prev !== event.content.membership && !isStateEvent) {
            if (isLiveEvent) {
                $rootScope.events.rooms[event.room_id].messages.push(event);
            }
            else {
                $rootScope.events.rooms[event.room_id].messages.unshift(event);
            }
        }

        // Use data from state event or the latest data from the stream.
        // Do not care of events that come when paginating back
        if (isStateEvent || isLiveEvent) {
            $rootScope.events.rooms[event.room_id].members[event.state_key] = event;
        }

        $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent);
    };

    var handlePresence = function(event, isLiveEvent) {
        $rootScope.presence[event.content.user_id] = event;
        $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
    };

    var handlePowerLevels = function(event, isLiveEvent) {
        initRoom(event.room_id);

        // Keep the latest data. Do not care of events that come when paginating back
        if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) {
            $rootScope.events.rooms[event.room_id][event.type] = event;
            $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);
        }
    };

    var handleRoomName = function(event, isLiveEvent) {
        console.log("handleRoomName " + isLiveEvent);

        initRoom(event.room_id);

        $rootScope.events.rooms[event.room_id][event.type] = event;
        $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
    };

    // TODO: Can this just be a generic "I am a room state event, can haz store?"
    var handleRoomTopic = function(event, isLiveEvent) {
        console.log("handleRoomTopic live="+isLiveEvent);

        initRoom(event.room_id);

        // live events always update, but non-live events only update if the
        // ts is later.
        if (!isLiveEvent) {
            var eventTs = event.ts;
            var storedEvent = $rootScope.events.rooms[event.room_id][event.type];
            if (storedEvent) {
                if (storedEvent.ts > eventTs) {
                    // ignore it, we have a newer one already.
                    return;
                }
            }
        }
        $rootScope.events.rooms[event.room_id][event.type] = event;
    };

    var handleCallEvent = function(event, isLiveEvent) {
        $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
        if (event.type == 'm.call.invite') {
            $rootScope.events.rooms[event.room_id].messages.push(event);
        }
    };

    /**
     * Get the index of the event in $rootScope.events.rooms[room_id].messages
     * @param {type} room_id the room id
     * @param {type} event_id the event id to look for
     * @returns {Number | undefined} the index. undefined if not found.
     */
    var getRoomEventIndex = function(room_id, event_id) {
        var index;

        var room = $rootScope.events.rooms[room_id];
        if (room) {
            // Start looking from the tail since the first goal of this function
            // is to find a messaged among the latest ones
            for (var i = room.messages.length - 1; i > 0; i--) {
                var message = room.messages[i];
                if (event_id === message.event_id) {
                    index = i;
                    break;
                }
            }
        }
        return index;
    }

    return {
        ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
        MSG_EVENT: MSG_EVENT,
        MEMBER_EVENT: MEMBER_EVENT,
        PRESENCE_EVENT: PRESENCE_EVENT,
        POWERLEVEL_EVENT: POWERLEVEL_EVENT,
        CALL_EVENT: CALL_EVENT,
        NAME_EVENT: NAME_EVENT,

        handleEvent: function(event, isLiveEvent, isStateEvent) {
            // Avoid duplicated events
            // Needed for rooms where initialSync has not been done.
            // In this case, we do not know where to start pagination. So, it starts from the END
            // and we can have the same event (ex: joined, invitation) coming from the pagination
            // AND from the event stream.
            // FIXME: This workaround should be no more required when /initialSync on a particular room
            // will be available (as opposite to the global /initialSync done at startup)
            if (!isStateEvent) {    // Do not consider state events
                if (event.event_id && eventMap[event.event_id]) {
                    console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4));
                    return;
                }
                else {
                    eventMap[event.event_id] = 1;
                }
            }

            if (event.type.indexOf('m.call.') === 0) {
                handleCallEvent(event, isLiveEvent);
            }
            else {
                switch(event.type) {
                    case "m.room.create":
                        handleRoomCreate(event, isLiveEvent);
                        break;
                    case "m.room.aliases":
                        handleRoomAliases(event, isLiveEvent);
                        break;
                    case "m.room.message":
                        handleMessage(event, isLiveEvent);
                        break;
                    case "m.room.member":
                        handleRoomMember(event, isLiveEvent, isStateEvent);
                        break;
                    case "m.presence":
                        handlePresence(event, isLiveEvent);
                        break;
                    case 'm.room.ops_levels':
                    case 'm.room.send_event_level':
                    case 'm.room.add_state_level':
                    case 'm.room.join_rules':
                    case 'm.room.power_levels':
                        handlePowerLevels(event, isLiveEvent);
                        break;
                    case 'm.room.name':
                        handleRoomName(event, isLiveEvent);
                        break;
                    case 'm.room.topic':
                        handleRoomTopic(event, isLiveEvent);
                        break;
                    default:
                        console.log("Unable to handle event type " + event.type);
                        console.log(JSON.stringify(event, undefined, 4));
                        break;
                }
            }
        },

        // isLiveEvents determines whether notifications should be shown, whether
        // messages get appended to the start/end of lists, etc.
        handleEvents: function(events, isLiveEvents, isStateEvents) {
            for (var i=0; i<events.length; i++) {
                this.handleEvent(events[i], isLiveEvents, isStateEvents);
            }
        },

        // Handle messages from /initialSync or /messages
        handleRoomMessages: function(room_id, messages, isLiveEvents) {
            this.handleEvents(messages.chunk, isLiveEvents);

            // Store how far back we've paginated
            // This assumes the paginations requests are contiguous and in reverse chronological order
            $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
        },

        handleInitialSyncDone: function(initialSyncData) {
            console.log("# handleInitialSyncDone");
            initialSyncDeferred.resolve(initialSyncData);
        },

        // Returns a promise that resolves when the initialSync request has been processed
        waitForInitialSyncCompletion: function() {
            return initialSyncDeferred.promise;
        },

        resetRoomMessages: function(room_id) {
            resetRoomMessages(room_id);
        }
    };
}]);
<|endoftext|>


<commit_before>describe('`defineModule` function', function() {

    var definedModule;

    it('should set namespace and override', function() {
        definedModule = ne.util.defineModule('foo.bar', {a: 'a'});
        expect(window.foo.bar).toBe(definedModule);

        definedModule = ne.util.defineModule('foo.bar', {b: 'b'});
        expect(window.foo.bar).toBe(definedModule);
        expect(window.foo.bar.a).not.toBeDefined();
        expect(window.foo.bar.b).toBeDefined();
    });

    describe('return a object', function() {
        var messagesForVerifying = {
                privateData: "I'm private data",
                publicData: "I'm public data",
                privateMethod: "I'm private method",
                publicMethod: "I'm public method",
                initializeMethod: "initialized!!"
            },
            base = {
                _privateData: messagesForVerifying.privateData,
                publicData: messagesForVerifying.publicData,
                initializationMessage: '',

                _privateMethod: function() {
                    return messagesForVerifying.privateMethod;
                },

                initialize: function() {
                    this.initializationMessage = messagesForVerifying.initializeMethod;
                },

                publicMethod: function() {
                    return messagesForVerifying.publicMethod;
                },

                getPrivateData: function() {
                    return this._privateData;
                },
                callPrivateMethod: function() {
                    return this._privateMethod();
                },

                changePublicData: function(data) {
                    this.publicData = data;
                }
            };

        beforeEach(function() {
            definedModule = ne.util.defineModule('foo.bar', base);
        });

        it('that should be initialized', function() {
            expect(definedModule.initializationMessage).toEqual(messagesForVerifying.initializeMethod);
        });

        it('that should not have `initialize` method', function() {
            expect(definedModule.initialize).not.toBeDefined();
        });

        it('that should have public data', function() {
            expect(definedModule.publicData).toEqual(messagesForVerifying.publicData);
        });

        it('that should work normally', function() {
            expect(definedModule.publicMethod()).toEqual(messagesForVerifying.publicMethod);
            expect(definedModule.getPrivateData()).toEqual(messagesForVerifying.privateData);
            expect(definedModule.callPrivateMethod()).toEqual(messagesForVerifying.privateMethod)

            definedModule.changePublicData('foo');
            expect(definedModule.publicData).toEqual('foo');
        });
    });
});
<commit_msg>code-review - 7<commit_after>describe('`defineModule` function', function() {

    var definedModule;

    it('should set namespace and override', function() {
        definedModule = ne.util.defineModule('foo.bar', {a: 'a'});
        expect(window.foo.bar).toBe(definedModule);

        definedModule = ne.util.defineModule('foo.bar', {b: 'b'});
        expect(window.foo.bar).toBe(definedModule);
        expect(window.foo.bar.a).not.toBeDefined();
        expect(window.foo.bar.b).toBeDefined();
    });

    describe('return a object', function() {
        var messagesForVerifying = {
                privateData: "I'm private data",
                publicData: "I'm public data",
                privateMethod: "I'm private method",
                publicMethod: "I'm public method",
                initializeMethod: "initialized!!"
            },
            base = {
                _privateData: messagesForVerifying.privateData,
                publicData: messagesForVerifying.publicData,
                initializationMessage: '',

                _privateMethod: function() {
                    return messagesForVerifying.privateMethod;
                },

                initialize: function() {
                    this.initializationMessage = messagesForVerifying.initializeMethod;
                },

                publicMethod: function() {
                    return messagesForVerifying.publicMethod;
                },

                getPrivateData: function() {
                    return this._privateData;
                },
                callPrivateMethod: function() {
                    return this._privateMethod();
                },

                changePublicData: function(data) {
                    this.publicData = data;
                }
            };

        beforeAll(function() {
            definedModule = ne.util.defineModule('foo.bar', base);
        });

        it('that should be initialized', function() {
            expect(definedModule.initializationMessage).toEqual(messagesForVerifying.initializeMethod);
        });

        it('that should not have `initialize` method', function() {
            expect(definedModule.initialize).not.toBeDefined();
        });

        it('that should have public data', function() {
            expect(definedModule.publicData).toEqual(messagesForVerifying.publicData);
        });

        it('that should work normally', function() {
            expect(definedModule.publicMethod()).toEqual(messagesForVerifying.publicMethod);
            expect(definedModule.getPrivateData()).toEqual(messagesForVerifying.privateData);
            expect(definedModule.callPrivateMethod()).toEqual(messagesForVerifying.privateMethod)

            definedModule.changePublicData('foo');
            expect(definedModule.publicData).toEqual('foo');
        });
    });
});
<|endoftext|>


<commit_before>/**
  * This file is part of taolin project (http://taolin.fbk.eu)
  * Copyright (C) 2008, 2009 FBK Foundation, (http://www.fbk.eu)
  * Authors: SoNet Group (see AUTHORS.txt)
  *
  * Taolin is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
  * the Free Software Foundation version 3 of the License.
  *
  * Taolin is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU Affero General Public License for more details.
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with Taolin. If not, see <http://www.gnu.org/licenses/>.
  *
  */

Ext.namespace('Ext.ux.fbk.sonet');

Ext.ux.fbk.sonet.WizardSettings = Ext.extend(Ext.form.FormPanel, {
    cls: 'form_settings'
    ,border: false
    ,autoScroll: true
    ,defaults: {
        // applied to each contained item
        autoWidth: true
        ,msgTarget: 'side'
    }
    ,monitorValid: true
    ,labelAlign: 'top'
    ,waitMsgTarget: false
    ,onRender:function(){
        Ext.ux.fbk.sonet.WizardSettings.superclass.onRender.apply(this, arguments);

        this.form.load({
            url: 'users/getusersettings/'
            ,text: "Loading..."
            ,success: this.setUserParams
            ,timeout: 60
            ,scope: this
        });
    }
    ,initComponent: function() {

        //this.userParams = {};

        this.view = new Ext.DataView({
            autoHeight: true
            ,multiSelect: false
            ,emptyText: 'Ouch... Something wrong happened here! Please report this error to '+window.config.contactus
            ,loadingText: 'Loading your personal information'
            ,itemSelector: 'wizard-user-profile'
            ,tpl: new Ext.XTemplate(
                '<tpl for=".">'
                    ,'
'
                        ,'
'
                            ,'
'
                                ,'<tpl if="photo != null">'
                                    ,''
                                ,'</tpl>'
                                ,'<tpl if="photo == null">'
                                    ,''
                                ,'</tpl>'
                            ,'
'
                            ,'
'
                                ,''
                                    ,'{name} {surname}'
                                ,''
                                ,'
'
                                    ,'
Login
'
                                    ,'
{login}
'
                                    ,'<tpl if="date_of_birth != null && date_of_birth != \'\'">'
                                        ,'
Date of birth
'
                                        ,'
{[Date.parseDate(values.date_of_birth, "Y-m-d").format("d/m/Y")]}
'
                                    ,'</tpl>'
                                    ,'
Email
'
                                    ,'<tpl if="(email != null && email != \'\')">'
                                        ,'
{email}
'
                                    ,'</tpl>'
                                    ,'<tpl if="email == null || email == \'\'">'
                                        ,'
{login}{[window.config.addtomail]}
'
                                    ,'</tpl>'
                                    ,'<tpl if="(groups_description != null && groups_description != \'\')">'
                                        ,'
{[window.config.defaultgroupname]}
'
                                        ,'
{groups_description}
'
                                    ,'</tpl>'
                                ,'
'
                            ,'
'
                        ,'
'
                        ,'
'
                            ,'Please edit your information below, in order to make your profile more complete and to let other people know more about you! The more information you provide, the easier it will be for your colleagues find useful information!

Please note that fields marked with (*) are mandatory'
                        ,'
'
                    ,'
'
                ,'</tpl>'
            )
            ,store: new Ext.data.SimpleStore({
                fields: [
                    {name: 'name'}, {name: 'surname'}, {name: 'photo'}, {name: 'login'}, {name: 'groups_description'}, null, null, null, null, null, {name: 'email'}, {name: 'date_of_birth'}
                ]
            })
        });

        this.wiz_form = new Ext.form.FieldSet({
            collapsible:false
            ,autoHeight: true
            ,autoWidth: true
            ,border:false
            ,defaultType: 'textfield'
            ,style:'padding:5px 0 0 5px'
            ,layoutConfig: {
                // layout-specific configs go here
                labelSeparator: ''
            }
            ,items: [{
                fieldLabel: 'Describe yourself with few keywords (*)
(Such as "Interaction design, nanotechnology, Italo-Germanic studies, darwinism, socialmedia, ...")'
                ,name: 'description'
                ,allowBlank: false
                ,xtype: 'textarea'
                ,grow: true
                ,anchor: '98%'
            }, {
                fieldLabel: 'Personal Page
(Such as your external blog or your FBK web page)'
                ,name: 'personal_page'
                ,vtype:'url'
                ,maxLength: 80
                ,anchor: '98%'
            }, {
                fieldLabel: 'Are you available for carpooling?
(The idea is to help your colleagues and you to coordinate for shared rides to and from work. More info about carpooling on Wikipedia)'
                ,name: 'carpooling'
                ,xtype: 'checkbox'
                ,anchor: '78%'
            }, {
                fieldLabel: 'Home address
(if you indicate that you are available for carpooling, please insert your home location.)'
                ,name: 'home_address'
                ,maxLength: 180
                ,anchor: '98%'
            }, {
                fieldLabel: ' Linkedin public profile
Example: set it tonickname if your linkedin public profile url is http://www.linkedin.com/in/nickname'
                ,name: 'linkedin'
                ,maxLength: 50
                ,anchor: '100%'
            }, {
                fieldLabel: ' Twitter username'
                ,name: 'twitter'
                ,maxLength: 50
                ,anchor: '100%'
            }, {
                fieldLabel: ' Facebook profile Web address
Once logged into Facebook, click the Profile button and then copy here the Web address of the resulting page'
                ,name: 'facebook'
                ,maxLength: 120
                ,vtype:'url'
                ,anchor: '100%'
            }]
        });

        var config = {
            url:'users/setusersettings'
            ,method: 'POST'
            ,items: [
                this.view
                ,this.wiz_form
           ]
        };

    Ext.apply(this, Ext.apply(this.initialConfig, config));

        Ext.ux.fbk.sonet.WizardSettings.superclass.initComponent.apply(this, arguments);
    }
    /*,onSuccess:function(form, action){
        console.log('Success');
    }*/
    ,onFailure:function(form, action){
        Ext.Msg.show({
            title: 'Error!',
            msg: 'Your data has not been saved!

Please check your data or submit a feedback to us.',
            width: 400,
            buttons: Ext.MessageBox.OK,
            icon: Ext.MessageBox.ERROR
        });
    }
    ,setUserParams:function(form, action){

        // userParams are used by the DataView
        this.userParams = [
            action.result.data.map(function(i){
                if(i.id)
                    return i.value;
            })
        ];

        this.view.store.loadData(this.userParams);
    }
});
<commit_msg>swapping linkedin and twitter icons<commit_after>/**
  * This file is part of taolin project (http://taolin.fbk.eu)
  * Copyright (C) 2008, 2009 FBK Foundation, (http://www.fbk.eu)
  * Authors: SoNet Group (see AUTHORS.txt)
  *
  * Taolin is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
  * the Free Software Foundation version 3 of the License.
  *
  * Taolin is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU Affero General Public License for more details.
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with Taolin. If not, see <http://www.gnu.org/licenses/>.
  *
  */

Ext.namespace('Ext.ux.fbk.sonet');

Ext.ux.fbk.sonet.WizardSettings = Ext.extend(Ext.form.FormPanel, {
    cls: 'form_settings'
    ,border: false
    ,autoScroll: true
    ,defaults: {
        // applied to each contained item
        autoWidth: true
        ,msgTarget: 'side'
    }
    ,monitorValid: true
    ,labelAlign: 'top'
    ,waitMsgTarget: false
    ,onRender:function(){
        Ext.ux.fbk.sonet.WizardSettings.superclass.onRender.apply(this, arguments);

        this.form.load({
            url: 'users/getusersettings/'
            ,text: "Loading..."
            ,success: this.setUserParams
            ,timeout: 60
            ,scope: this
        });
    }
    ,initComponent: function() {

        //this.userParams = {};

        this.view = new Ext.DataView({
            autoHeight: true
            ,multiSelect: false
            ,emptyText: 'Ouch... Something wrong happened here! Please report this error to '+window.config.contactus
            ,loadingText: 'Loading your personal information'
            ,itemSelector: 'wizard-user-profile'
            ,tpl: new Ext.XTemplate(
                '<tpl for=".">'
                    ,'
'
                        ,'
'
                            ,'
'
                                ,'<tpl if="photo != null">'
                                    ,''
                                ,'</tpl>'
                                ,'<tpl if="photo == null">'
                                    ,''
                                ,'</tpl>'
                            ,'
'
                            ,'
'
                                ,''
                                    ,'{name} {surname}'
                                ,''
                                ,'
'
                                    ,'
Login
'
                                    ,'
{login}
'
                                    ,'<tpl if="date_of_birth != null && date_of_birth != \'\'">'
                                        ,'
Date of birth
'
                                        ,'
{[Date.parseDate(values.date_of_birth, "Y-m-d").format("d/m/Y")]}
'
                                    ,'</tpl>'
                                    ,'
Email
'
                                    ,'<tpl if="(email != null && email != \'\')">'
                                        ,'
{email}
'
                                    ,'</tpl>'
                                    ,'<tpl if="email == null || email == \'\'">'
                                        ,'
{login}{[window.config.addtomail]}
'
                                    ,'</tpl>'
                                    ,'<tpl if="(groups_description != null && groups_description != \'\')">'
                                        ,'
{[window.config.defaultgroupname]}
'
                                        ,'
{groups_description}
'
                                    ,'</tpl>'
                                ,'
'
                            ,'
'
                        ,'
'
                        ,'
'
                            ,'Please edit your information below, in order to make your profile more complete and to let other people know more about you! The more information you provide, the easier it will be for your colleagues find useful information!

Please note that fields marked with (*) are mandatory'
                        ,'
'
                    ,'
'
                ,'</tpl>'
            )
            ,store: new Ext.data.SimpleStore({
                fields: [
                    {name: 'name'}, {name: 'surname'}, {name: 'photo'}, {name: 'login'}, {name: 'groups_description'}, null, null, null, null, null, {name: 'email'}, {name: 'date_of_birth'}
                ]
            })
        });

        this.wiz_form = new Ext.form.FieldSet({
            collapsible:false
            ,autoHeight: true
            ,autoWidth: true
            ,border:false
            ,defaultType: 'textfield'
            ,style:'padding:5px 0 0 5px'
            ,layoutConfig: {
                // layout-specific configs go here
                labelSeparator: ''
            }
            ,items: [{
                fieldLabel: 'Describe yourself with few keywords (*)
(Such as "Interaction design, nanotechnology, Italo-Germanic studies, darwinism, socialmedia, ...")'
                ,name: 'description'
                ,allowBlank: false
                ,xtype: 'textarea'
                ,grow: true
                ,anchor: '98%'
            }, {
                fieldLabel: 'Personal Page
(Such as your external blog or your FBK web page)'
                ,name: 'personal_page'
                ,vtype:'url'
                ,maxLength: 80
                ,anchor: '98%'
            }, {
                fieldLabel: 'Are you available for carpooling?
(The idea is to help your colleagues and you to coordinate for shared rides to and from work. More info about carpooling on Wikipedia)'
                ,name: 'carpooling'
                ,xtype: 'checkbox'
                ,anchor: '78%'
            }, {
                fieldLabel: 'Home address
(if you indicate that you are available for carpooling, please insert your home location.)'
                ,name: 'home_address'
                ,maxLength: 180
                ,anchor: '98%'
            }, {
                fieldLabel: ' Linkedin public profile
Example: set it tonickname if your linkedin public profile url is http://www.linkedin.com/in/nickname'
                ,name: 'linkedin'
                ,maxLength: 50
                ,anchor: '100%'
            }, {
                fieldLabel: ' Twitter username'
                ,name: 'twitter'
                ,maxLength: 50
                ,anchor: '100%'
            }, {
                fieldLabel: ' Facebook profile Web address
Once logged into Facebook, click the Profile button and then copy here the Web address of the resulting page'
                ,name: 'facebook'
                ,maxLength: 120
                ,vtype:'url'
                ,anchor: '100%'
            }]
        });

        var config = {
            url:'users/setusersettings'
            ,method: 'POST'
            ,items: [
                this.view
                ,this.wiz_form
           ]
        };

    Ext.apply(this, Ext.apply(this.initialConfig, config));

        Ext.ux.fbk.sonet.WizardSettings.superclass.initComponent.apply(this, arguments);
    }
    /*,onSuccess:function(form, action){
        console.log('Success');
    }*/
    ,onFailure:function(form, action){
        Ext.Msg.show({
            title: 'Error!',
            msg: 'Your data has not been saved!

Please check your data or submit a feedback to us.',
            width: 400,
            buttons: Ext.MessageBox.OK,
            icon: Ext.MessageBox.ERROR
        });
    }
    ,setUserParams:function(form, action){

        // userParams are used by the DataView
        this.userParams = [
            action.result.data.map(function(i){
                if(i.id)
                    return i.value;
            })
        ];

        this.view.store.loadData(this.userParams);
    }
});
<|endoftext|>


<commit_before>import path from 'path'
import {remote} from 'electron'
import md from 'utils/markdown'
import wordCount from 'word-count'

const win = remote.getCurrentWindow()
win.$state.unsaved = 0

const renderHTML = tab => {
  return md.render(tab.content).replace(/src="([^"]+)"/g, (m, p1) => {
    if (p1[0] === '.') {
      p1 = path.join(path.dirname(tab.filePath), p1)
      return `src="${p1}"`
    }
    return m
  })
}

const state = {
  tabs: [],
  draggingTab: false,
  currentTabIndex: 0
}

const mutations = {
  INIT_NEW_TAB(state, payload) {
    const tab = {
      ...payload,
      html: renderHTML(payload),
      wordCount: wordCount(payload.content)
    }
    state.tabs.push(tab)
    state.currentTabIndex++
  },
  UPDATE_CONTENT(state, {index, content}) {
    const tab = state.tabs[index]
    tab.content = content
    tab.html = renderHTML(tab)
    tab.wordCount = wordCount(tab.content)
  },
  UPDATE_FILE_PATH(state, {index, filePath}) {
    const tab = state.tabs[index]
    tab.filePath = filePath
    document.title = `${path.basename(filePath)} - EME`
  },
  UPDATE_CONTENT_WITH_FILEPATH(state, {index, content, filePath}) {
    const tab = state.tabs[index]
    tab.content = content
    tab.html = renderHTML({
      ...tab,
      content,
      filePath
    })
    tab.filePath = filePath
    document.title = `${path.basename(filePath)} - EME`
  },
  UPDATE_SAVE_STATUS(state, {index, saved}) {
    const tab = state.tabs[index]
    const fileName = tab.filePath ?
      path.basename(tab.filePath) :
      'untitled'

    if (saved) {
      if (saved !== tab.saved) win.$state.unsaved--
      document.title = `${fileName} - EME`
    } else {
      if (saved !== tab.saved) win.$state.unsaved++
      document.title = `${fileName} * - EME`
    }

    tab.saved = saved
  },
  UPDATE_RENAME_STATUS(state, {index, rename}) {
    state.tabs[index].rename = rename
  },
  UPDATE_DRAGGING_STATUS(state, dragging) {
    state.draggingTab = dragging
  },
  SET_EDITOR(state, {index, editor}) {
    state.currentTabIndex = index
    state.tabs[index].editor = editor
  },
  SET_CURRENT_TAB(state, index) {
    state.currentTabIndex = index
  },
  CLOSE_TAB(state, indexToClose) {
    if (state.currentTabIndex !== 0 && indexToClose <= state.currentTabIndex) {
      state.currentTabIndex--
    }
    setTimeout(() => {
      const tab = state.tabs[state.currentTabIndex]
      if (tab && tab.editor) {
        tab.editor.refresh()
        tab.editor.focus()
      }
    }, 0)
    state.tabs = state.tabs.filter((tab, index) => {
      return index !== indexToClose
    })
  },
  SET_WRITING_MODE(state, {index, mode}) {
    const tab = state.tabs[index]
    tab.writingMode = mode
    setTimeout(() => {
      tab.editor.refresh()
      if (mode !== 'preview') tab.editor.focus()
    }, 0)
  },
  UPDATE_PDF(state, {index, pdf}) {
    const tab = state.tabs[index]
    tab.pdf = pdf
  },
  REORDER_TABS(state, {oldIndex, newIndex}) {
    const tabs = state.tabs
    if (newIndex >= tabs.length) {
      let k = newIndex - tabs.length
      while ((k--) + 1) {
        tabs.push(undefined)
      }
    }
    tabs.splice(newIndex, 0, tabs.splice(oldIndex, 1)[0])
    state.currentTabIndex = newIndex
  }
}

export default {
  state,
  mutations
}
<commit_msg>refocus editor after tab resorted<commit_after>import path from 'path'
import {remote} from 'electron'
import md from 'utils/markdown'
import wordCount from 'word-count'

const win = remote.getCurrentWindow()
win.$state.unsaved = 0

const focusEditor = (tabs, index) => {
  const tab = tabs[index]
  if (tab && tab.editor) {
    tab.editor.refresh()
    tab.editor.focus()
  }
}

const renderHTML = tab => {
  return md.render(tab.content).replace(/src="([^"]+)"/g, (m, p1) => {
    if (p1[0] === '.') {
      p1 = path.join(path.dirname(tab.filePath), p1)
      return `src="${p1}"`
    }
    return m
  })
}

const state = {
  tabs: [],
  draggingTab: false,
  currentTabIndex: 0
}

const mutations = {
  INIT_NEW_TAB(state, payload) {
    const tab = {
      ...payload,
      html: renderHTML(payload),
      wordCount: wordCount(payload.content)
    }
    state.tabs.push(tab)
    state.currentTabIndex++
  },
  UPDATE_CONTENT(state, {index, content}) {
    const tab = state.tabs[index]
    tab.content = content
    tab.html = renderHTML(tab)
    tab.wordCount = wordCount(tab.content)
  },
  UPDATE_FILE_PATH(state, {index, filePath}) {
    const tab = state.tabs[index]
    tab.filePath = filePath
    document.title = `${path.basename(filePath)} - EME`
  },
  UPDATE_CONTENT_WITH_FILEPATH(state, {index, content, filePath}) {
    const tab = state.tabs[index]
    tab.content = content
    tab.html = renderHTML({
      ...tab,
      content,
      filePath
    })
    tab.filePath = filePath
    document.title = `${path.basename(filePath)} - EME`
  },
  UPDATE_SAVE_STATUS(state, {index, saved}) {
    const tab = state.tabs[index]
    const fileName = tab.filePath ?
      path.basename(tab.filePath) :
      'untitled'

    if (saved) {
      if (saved !== tab.saved) win.$state.unsaved--
      document.title = `${fileName} - EME`
    } else {
      if (saved !== tab.saved) win.$state.unsaved++
      document.title = `${fileName} * - EME`
    }

    tab.saved = saved
  },
  UPDATE_RENAME_STATUS(state, {index, rename}) {
    state.tabs[index].rename = rename
  },
  UPDATE_DRAGGING_STATUS(state, dragging) {
    state.draggingTab = dragging
  },
  SET_EDITOR(state, {index, editor}) {
    state.currentTabIndex = index
    state.tabs[index].editor = editor
  },
  SET_CURRENT_TAB(state, index) {
    state.currentTabIndex = index
  },
  CLOSE_TAB(state, indexToClose) {
    if (state.currentTabIndex !== 0 && indexToClose <= state.currentTabIndex) {
      state.currentTabIndex--
    }
    setTimeout(() => {
      focusEditor(state.tabs, state.currentTabIndex)
    }, 0)
    state.tabs = state.tabs.filter((tab, index) => {
      return index !== indexToClose
    })
  },
  SET_WRITING_MODE(state, {index, mode}) {
    const tab = state.tabs[index]
    tab.writingMode = mode
    setTimeout(() => {
      tab.editor.refresh()
      if (mode !== 'preview') tab.editor.focus()
    }, 0)
  },
  UPDATE_PDF(state, {index, pdf}) {
    const tab = state.tabs[index]
    tab.pdf = pdf
  },
  REORDER_TABS(state, {oldIndex, newIndex}) {
    const tabs = state.tabs
    if (newIndex >= tabs.length) {
      let k = newIndex - tabs.length
      while ((k--) + 1) {
        tabs.push(undefined)
      }
    }
    tabs.splice(newIndex, 0, tabs.splice(oldIndex, 1)[0])
    state.currentTabIndex = newIndex
    setTimeout(() => {
      focusEditor(state.tabs, state.currentTabIndex)
    }, 0)
  }
}

export default {
  state,
  mutations
}
<|endoftext|>


<commit_before>import React from 'react';

import { formatPercentage } from 'common/format';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import Analyzer from 'parser/core/Analyzer';
import Haste from 'parser/core/modules/Haste';
import StatisticBox, { STATISTIC_ORDER } from 'interface/others/StatisticBox';
import Tab from 'interface/others/Tab';

import Insanity from '../core/Insanity';
import VoidformsTab from './VoidformsTab';

const debug = false;
const logger = (message, color) => debug && console.log(`%c${message.join('  ')}`, `color: ${color}`);

const VOID_FORM_ACTIVATORS = [
  SPELLS.VOID_ERUPTION.id,
  SPELLS.DARK_ASCENSION_TALENT.id,
];

class Voidform extends Analyzer {
  static dependencies = {
    insanity: Insanity,
    haste: Haste,
  };

  _previousVoidformCast = null;
  _totalHasteAcquiredOutsideVoidform = 0;
  _totalLingeringInsanityTimeOutsideVoidform = 0;
  _inVoidform = false;

  _voidforms = {};

  get voidforms() {
    return Object.keys(this._voidforms).map(key => this._voidforms[key]);
  }

  get nonExcludedVoidforms() {
    return this.voidforms.filter(voidform => !voidform.excluded);
  }

  get averageVoidformStacks() {
    if (this.voidforms.length === 0) {
      return 0;
    }
    // ignores last voidform if seen as skewing
    return this.nonExcludedVoidforms.reduce((p, c) => p + c.stacks.length, 0) / this.nonExcludedVoidforms.length;
  }

  get averageVoidformHaste() {
    if (!this.currentVoidform) {
      return (1 + this.haste.current);
    }

    const averageHasteGainedFromVoidform = (this.voidforms.reduce((total, voidform) => total + voidform.averageGainedHaste, 0)) / this.voidforms.length;
    return (1 + this.haste.current) * (1 + averageHasteGainedFromVoidform);
  }

  get averageNonVoidformHaste() {
    return (1 + this.haste.current) * (1 + (this._totalHasteAcquiredOutsideVoidform / this._totalLingeringInsanityTimeOutsideVoidform) / 100);
  }

  get inVoidform() {
    return this.selectedCombatant.hasBuff(SPELLS.VOIDFORM_BUFF.id);
  }

  get currentVoidform() {
    if (this.voidforms && this.voidforms.length > 0) {
      return this._voidforms[this.voidforms[this.voidforms.length - 1].start];
    } else {
      return false;
    }
  }

  get uptime() {
    return this.selectedCombatant.getBuffUptime(SPELLS.VOIDFORM_BUFF.id) / (this.owner.fightDuration - this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id));
  }

  get normalizeTimestamp() {
    return (event) => Math.round((event.timestamp - this.currentVoidform.start) / 10) * 10;
  }

  get addVoidformEvent() {
    return (name, event) => {
      if (this.currentVoidform) {
        this.currentVoidform[name] = [
          ...this.currentVoidform[name],
          event,
        ];
      }
    };
  }

  addVoidformStack(event) {
    if (!this.currentVoidform) return;
    this.currentVoidform.stacks = [
      ...this.currentVoidform.stacks,
      { stack: event.stack, timestamp: this.normalizeTimestamp(event) },
    ];
    logger(['Added voidform stack:', event.stack, `at`, this.normalizeTimestamp(event)], 'green');
  }

  removeLingeringInsanityStack(event) {
    if (this.inVoidform) {
      this.currentVoidform.lingeringInsanityStacks = [
        ...this.currentVoidform.lingeringInsanityStacks,
        { stack: event.stack, timestamp: this.normalizeTimestamp(event) },
      ];
      logger(['Removed lingering stack:', event.stack, 'at', this.normalizeTimestamp(event)], 'orange');
    } else {
      this._totalHasteAcquiredOutsideVoidform += event.stack;
      this._totalLingeringInsanityTimeOutsideVoidform += 1;
    }
  }

  startVoidform(event) {
    this._voidforms[event.timestamp] = {
      start: event.timestamp,
      lingeringInsanityStacks: [],
      stacks: [],
      excluded: false,
      averageGainedHaste: 0,
      [SPELLS.MINDBENDER_TALENT_SHADOW.id]: [],
      [SPELLS.VOID_TORRENT_TALENT.id]: [],
      [SPELLS.DISPERSION.id]: [],
    };
    logger(['Started voidform at:', event.timestamp], 'purple');
    this.addVoidformStack({ ...event, stack: 1 });
  }

  endVoidform(event) {
    this.currentVoidform.duration = this.normalizeTimestamp(event);

    // artificially adds the starting lingering insanity stack:
    if (this.currentVoidform.lingeringInsanityStacks.length > 0) {
      const { stack: nextStack } = this.currentVoidform.lingeringInsanityStacks[0];
      this.currentVoidform.lingeringInsanityStacks = [
        { stack: nextStack + 2, timestamp: 0 },
        ...this.currentVoidform.lingeringInsanityStacks,
      ];
    }

    // calculates the average gained haste from voidform stacks & lingering insanity within the voidform:
    this.currentVoidform.averageGainedHaste = (this.currentVoidform.stacks.reduce((total, { stack, timestamp }, i) => {
      const nextTimestamp = this.currentVoidform.stacks[i + 1] ? this.currentVoidform.stacks[i + 1].timestamp : timestamp + 1000;
      return total + ((nextTimestamp - timestamp) / 1000) * stack / 100;
    }, 0) + this.currentVoidform.lingeringInsanityStacks.reduce((total, { stack, timestamp }, i) => {
      const nextTimestamp = this.currentVoidform.lingeringInsanityStacks[i + 1] ? this.currentVoidform.lingeringInsanityStacks[i + 1].timestamp : timestamp + 1000;
      return total + ((nextTimestamp - timestamp) / 1000) * stack / 100;
    }, 0)) / (this.currentVoidform.duration / 1000);
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (VOID_FORM_ACTIVATORS.includes(spellId)) {
      this.startVoidform(event);
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.VOIDFORM_BUFF.id) {
      this.endVoidform(event);
    }
  }

  on_byPlayer_applybuffstack(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.VOIDFORM_BUFF.id) {
      if (!this.currentVoidform) {
        // for prepull voidforms
        this.startVoidform(event);
      }
      this.addVoidformStack(event);
    }
  }

  on_byPlayer_removebuffstack(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.LINGERING_INSANITY.id) this.removeLingeringInsanityStack(event);
  }

  on_finished() {
    if (this.selectedCombatant.hasBuff(SPELLS.VOIDFORM_BUFF.id)) {
      // excludes last one to avoid skewing the average (if in voidform when the encounter ends):
      const averageVoidformStacks = this.voidforms.slice(0, -1).reduce((p, c) => p + c.stacks.length, 0) / (this.voidforms.length - 1);
      const lastVoidformStacks = this.currentVoidform.stacks.length;

      if (lastVoidformStacks + 5 < averageVoidformStacks) {
        this.currentVoidform.excluded = true;
      }

      // end last voidform of the fight:
      this.endVoidform({ timestamp: this.owner._timestamp });
    }

    debug && console.log(this.voidforms);
  }

  get suggestionUptimeThresholds() {
    return {
      actual: this.uptime,
      isLessThan: {
        minor: 0.7,
        average: 0.65,
        major: 0.6,
      },
      style: 'percentage',
    };
  }

  get suggestionStackThresholds() {
    return (voidform) => ({
      actual: voidform.stacks.length,
      isLessThan: {
        minor: 22,
        average: 20,
        major: 18,
      },
      style: 'number',
    });
  }

  suggestions(when) {
    const {
      isLessThan: {
        minor,
        average,
        major,
      },
    } = this.suggestionUptimeThresholds;

    when(this.uptime).isLessThan(minor)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(Your <SpellLink id={SPELLS.VOIDFORM.id} /> uptime can be improved. Try to maximize the uptime by using your insanity generating spells and cast <SpellLink id={SPELLS.MINDBENDER_TALENT_SHADOW.id} /> on cooldown.

          Use the generators with the priority:

<SpellLink id={SPELLS.VOID_BOLT.id} />

<SpellLink id={SPELLS.MIND_BLAST.id} />

<SpellLink id={SPELLS.MIND_FLAY.id} />
        )
          .icon(SPELLS.VOIDFORM_BUFF.icon)
          .actual(`${formatPercentage(actual)}% Voidform uptime`)
          .recommended(`>${formatPercentage(recommended)}% is recommended`)
          .regular(average).major(major);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(1)}
        icon={<SpellIcon id={SPELLS.VOIDFORM.id} />}
        value={`${formatPercentage(this.selectedCombatant.getBuffUptime(SPELLS.VOIDFORM_BUFF.id) / (this.owner.fightDuration - this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id)))} %`}
        label={(
          <dfn data-tip={`Time spent in dispersion (${Math.round(this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id) / 1000)} seconds) is excluded from the fight.`}>
            Voidform uptime
          </dfn>
        )}
      />
    );
  }

  tab() {
    return {
      title: 'Voidforms',
      url: 'voidforms',
      render: () => (
        <Tab>
          <VoidformsTab
            voidforms={this.voidforms}
            insanityEvents={this.insanity.events}
            fightEnd={this.owner.fight.end_time}
            surrenderToMadness={!!this.selectedCombatant.hasTalent(SPELLS.SURRENDER_TO_MADNESS_TALENT.id)}
          />
        </Tab>
      ),
    };
  }
}

export default Voidform;
<commit_msg>updating uptime<commit_after>import React from 'react';

import { formatPercentage } from 'common/format';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import Analyzer from 'parser/core/Analyzer';
import Haste from 'parser/core/modules/Haste';
import StatisticBox, { STATISTIC_ORDER } from 'interface/others/StatisticBox';
import Tab from 'interface/others/Tab';

import Insanity from '../core/Insanity';
import VoidformsTab from './VoidformsTab';

const debug = false;
const logger = (message, color) => debug && console.log(`%c${message.join('  ')}`, `color: ${color}`);

const VOID_FORM_ACTIVATORS = [
  SPELLS.VOID_ERUPTION.id,
  SPELLS.DARK_ASCENSION_TALENT.id,
];

class Voidform extends Analyzer {
  static dependencies = {
    insanity: Insanity,
    haste: Haste,
  };

  _previousVoidformCast = null;
  _totalHasteAcquiredOutsideVoidform = 0;
  _totalLingeringInsanityTimeOutsideVoidform = 0;
  _inVoidform = false;

  _voidforms = {};

  get voidforms() {
    return Object.keys(this._voidforms).map(key => this._voidforms[key]);
  }

  get nonExcludedVoidforms() {
    return this.voidforms.filter(voidform => !voidform.excluded);
  }

  get averageVoidformStacks() {
    if (this.voidforms.length === 0) {
      return 0;
    }
    // ignores last voidform if seen as skewing
    return this.nonExcludedVoidforms.reduce((p, c) => p + c.stacks.length, 0) / this.nonExcludedVoidforms.length;
  }

  get averageVoidformHaste() {
    if (!this.currentVoidform) {
      return (1 + this.haste.current);
    }

    const averageHasteGainedFromVoidform = (this.voidforms.reduce((total, voidform) => total + voidform.averageGainedHaste, 0)) / this.voidforms.length;
    return (1 + this.haste.current) * (1 + averageHasteGainedFromVoidform);
  }

  get averageNonVoidformHaste() {
    return (1 + this.haste.current) * (1 + (this._totalHasteAcquiredOutsideVoidform / this._totalLingeringInsanityTimeOutsideVoidform) / 100);
  }

  get inVoidform() {
    return this.selectedCombatant.hasBuff(SPELLS.VOIDFORM_BUFF.id);
  }

  get currentVoidform() {
    if (this.voidforms && this.voidforms.length > 0) {
      return this._voidforms[this.voidforms[this.voidforms.length - 1].start];
    } else {
      return false;
    }
  }

  get uptime() {
    return this.selectedCombatant.getBuffUptime(SPELLS.VOIDFORM_BUFF.id) / (this.owner.fightDuration - this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id));
  }

  get normalizeTimestamp() {
    return (event) => Math.round((event.timestamp - this.currentVoidform.start) / 10) * 10;
  }

  get addVoidformEvent() {
    return (name, event) => {
      if (this.currentVoidform) {
        this.currentVoidform[name] = [
          ...this.currentVoidform[name],
          event,
        ];
      }
    };
  }

  addVoidformStack(event) {
    if (!this.currentVoidform) return;
    this.currentVoidform.stacks = [
      ...this.currentVoidform.stacks,
      { stack: event.stack, timestamp: this.normalizeTimestamp(event) },
    ];
    logger(['Added voidform stack:', event.stack, `at`, this.normalizeTimestamp(event)], 'green');
  }

  removeLingeringInsanityStack(event) {
    if (this.inVoidform) {
      this.currentVoidform.lingeringInsanityStacks = [
        ...this.currentVoidform.lingeringInsanityStacks,
        { stack: event.stack, timestamp: this.normalizeTimestamp(event) },
      ];
      logger(['Removed lingering stack:', event.stack, 'at', this.normalizeTimestamp(event)], 'orange');
    } else {
      this._totalHasteAcquiredOutsideVoidform += event.stack;
      this._totalLingeringInsanityTimeOutsideVoidform += 1;
    }
  }

  startVoidform(event) {
    this._voidforms[event.timestamp] = {
      start: event.timestamp,
      lingeringInsanityStacks: [],
      stacks: [],
      excluded: false,
      averageGainedHaste: 0,
      [SPELLS.MINDBENDER_TALENT_SHADOW.id]: [],
      [SPELLS.VOID_TORRENT_TALENT.id]: [],
      [SPELLS.DISPERSION.id]: [],
    };
    logger(['Started voidform at:', event.timestamp], 'purple');
    this.addVoidformStack({ ...event, stack: 1 });
  }

  endVoidform(event) {
    this.currentVoidform.duration = this.normalizeTimestamp(event);

    // artificially adds the starting lingering insanity stack:
    if (this.currentVoidform.lingeringInsanityStacks.length > 0) {
      const { stack: nextStack } = this.currentVoidform.lingeringInsanityStacks[0];
      this.currentVoidform.lingeringInsanityStacks = [
        { stack: nextStack + 2, timestamp: 0 },
        ...this.currentVoidform.lingeringInsanityStacks,
      ];
    }

    // calculates the average gained haste from voidform stacks & lingering insanity within the voidform:
    this.currentVoidform.averageGainedHaste = (this.currentVoidform.stacks.reduce((total, { stack, timestamp }, i) => {
      const nextTimestamp = this.currentVoidform.stacks[i + 1] ? this.currentVoidform.stacks[i + 1].timestamp : timestamp + 1000;
      return total + ((nextTimestamp - timestamp) / 1000) * stack / 100;
    }, 0) + this.currentVoidform.lingeringInsanityStacks.reduce((total, { stack, timestamp }, i) => {
      const nextTimestamp = this.currentVoidform.lingeringInsanityStacks[i + 1] ? this.currentVoidform.lingeringInsanityStacks[i + 1].timestamp : timestamp + 1000;
      return total + ((nextTimestamp - timestamp) / 1000) * stack / 100;
    }, 0)) / (this.currentVoidform.duration / 1000);
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (VOID_FORM_ACTIVATORS.includes(spellId)) {
      this.startVoidform(event);
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.VOIDFORM_BUFF.id) {
      this.endVoidform(event);
    }
  }

  on_byPlayer_applybuffstack(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.VOIDFORM_BUFF.id) {
      if (!this.currentVoidform) {
        // for prepull voidforms
        this.startVoidform(event);
      }
      this.addVoidformStack(event);
    }
  }

  on_byPlayer_removebuffstack(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.LINGERING_INSANITY.id) this.removeLingeringInsanityStack(event);
  }

  on_finished() {
    if (this.selectedCombatant.hasBuff(SPELLS.VOIDFORM_BUFF.id)) {
      // excludes last one to avoid skewing the average (if in voidform when the encounter ends):
      const averageVoidformStacks = this.voidforms.slice(0, -1).reduce((p, c) => p + c.stacks.length, 0) / (this.voidforms.length - 1);
      const lastVoidformStacks = this.currentVoidform.stacks.length;

      if (lastVoidformStacks + 5 < averageVoidformStacks) {
        this.currentVoidform.excluded = true;
      }

      // end last voidform of the fight:
      this.endVoidform({ timestamp: this.owner._timestamp });
    }

    debug && console.log(this.voidforms);
  }

  get suggestionUptimeThresholds() {
    return {
      actual: this.uptime,
      isLessThan: {
        minor: 0.7,
        average: 0.65,
        major: 0.6,
      },
      style: 'percentage',
    };
  }

  get suggestionStackThresholds() {
    return (voidform) => ({
      actual: voidform.stacks.length,
      isLessThan: {
        minor: 20,
        average: 19,
        major: 18,
      },
      style: 'number',
    });
  }

  suggestions(when) {
    const {
      isLessThan: {
        minor,
        average,
        major,
      },
    } = this.suggestionUptimeThresholds;

    when(this.uptime).isLessThan(minor)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(Your <SpellLink id={SPELLS.VOIDFORM.id} /> uptime can be improved. Try to maximize the uptime by using your insanity generating spells and cast <SpellLink id={SPELLS.MINDBENDER_TALENT_SHADOW.id} /> on cooldown.

          Use the generators with the priority:

<SpellLink id={SPELLS.VOID_BOLT.id} />

<SpellLink id={SPELLS.MIND_BLAST.id} />

<SpellLink id={SPELLS.MIND_FLAY.id} />
        )
          .icon(SPELLS.VOIDFORM_BUFF.icon)
          .actual(`${formatPercentage(actual)}% Voidform uptime`)
          .recommended(`>${formatPercentage(recommended)}% is recommended`)
          .regular(average).major(major);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(1)}
        icon={<SpellIcon id={SPELLS.VOIDFORM.id} />}
        value={`${formatPercentage(this.selectedCombatant.getBuffUptime(SPELLS.VOIDFORM_BUFF.id) / (this.owner.fightDuration - this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id)))} %`}
        label={(
          <dfn data-tip={`Time spent in dispersion (${Math.round(this.selectedCombatant.getBuffUptime(SPELLS.DISPERSION.id) / 1000)} seconds) is excluded from the fight.`}>
            Voidform uptime
          </dfn>
        )}
      />
    );
  }

  tab() {
    return {
      title: 'Voidforms',
      url: 'voidforms',
      render: () => (
        <Tab>
          <VoidformsTab
            voidforms={this.voidforms}
            insanityEvents={this.insanity.events}
            fightEnd={this.owner.fight.end_time}
            surrenderToMadness={!!this.selectedCombatant.hasTalent(SPELLS.SURRENDER_TO_MADNESS_TALENT.id)}
          />
        </Tab>
      ),
    };
  }
}

export default Voidform;
<|endoftext|>


<commit_before>/** The FlamingLayer Class **/

/**
 * @constructor
 * @augments Layer
 * @description The superclass for all flamingolayers
 * @param id The id of the layer
 * @param options The options to be given to the layer
 * @param flamingoObject The flamingo object of the layer
 *
 */

Ext.define("viewer.viewercontroller.flamingo.FlamingoLayer",{
    enabledEvents: null,
    type: null,
    constructor :function (config){
        if (config.id){
            this.id = config.viewerController.mapComponent.makeFlamingoAcceptableId(config.id);
            config.id = this.id;
        }
        this.enabledEvents=new Object();
        return this;
    },
    /**
     * Get's the frameworklayer: the viewer specific layer.
     */
    getFrameworkLayer : function(){
        if (this.map==null){
            return null;
        }
        return this.map.getFrameworkMap();
    },

    toXML : function(){
        Ext.Error.raise({msg: "FlamingoLayer.toXML(): .toXML() must be made!"});
    },

    getTagName : function(){
        Ext.Error.raise({msg: "FlamingoLayer.getTagName: .getTagName() must be made!"});
    },
    setOption : function(optionKey,optionValue){
        this.options[optionKey]=optionValue;
    },
    getId : function(){
        return this.id;
    },
    getFrameworkId: function(){
        return this.map.getId()+"_"+this.getId();
    },
    setAlpha : function (alpha){
        // Fixup wrong Flash alpha, it thinks 0 is transparent and 100 is opaque
        if(this.map) {
            this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"setAlpha",alpha)
        }
    },
    getAlpha : function (){
        return this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"getAlpha");
    },
    /**
     * Get the last getMap request array
     * @see viewer.viewerController.controller.Layer#getLastMapRequest
     */
    getLastMapRequest: function(){
        var request=this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"getLastGetMapRequest");
        if (request==null){
            return null;
        }
        return [request];
    },
    /**
     * Returns the type of the layer.
     */
    getType: function(){
        return this.type;
    },
    /**
     * reloads the framework map
     */
    reload: function(){
        if (this.map.getFrameworkMap()){
            return this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"update");
        }
    },
    /**
     * Overwrites the addListener function. Add's the event to allowexternalinterface of flamingo
     * so flamingo is allowed to broadcast the event.
     */
    addListener : function(event,handler,scope){
        //enable flamingo event broadcasting
        var flamEvent=this.map.mapComponent.eventList[event];
        if (flamEvent!=undefined){
            //if not enabled yet, add it
            if (this.enabledEvents[flamEvent]==undefined){
               this.map.getFrameworkMap().callMethod(this.map.mapComponent.getId(),"addAllowExternalInterface",this.getFrameworkId()+"."+flamEvent);
               this.enabledEvents[flamEvent]=true;
            }
        }
        /* fix for infinite loop:
         * If this is called from a layer that extends the FlamingoArcLayer the superclass is
         * that FlamingoArcLayer and this function is called again when this.superclass.function is called
         **/
        if (this.superclass.$className == "viewer.viewercontroller.flamingo.FlamingoArcLayer"){
            this.superclass.superclass.addListener.call(this,event,handler,scope);
        }else{
            this.superclass.addListener.call(this,event,handler,scope);
        }
        //viewer.viewercontroller.controller.Layer.superclass.addListener.call(this,event,handler,scope);
    },
    setVisible : function (visible){
        this.visible = visible;

        if (this.options!=null){
            this.options["visible"] = visible;
        }

        if (this.map !=null){
            this.map.getFrameworkMap().callMethod(this.map.id + "_" + this.id, "setVisible", visible);
        }
    },
    /**
     * Get the visibility
     */
    getVisible : function (){
        if (this.map !=null){
            this.visible = this.map.getFrameworkMap().callMethod(this.map.id + "_" + this.id, "getVisible");
        }
        if (this.options!=null)
            this.options["visible"] = this.visible;
        return this.visible;
    },
    /**
     * Overwrite destroy, clear Listeners and forward to super.destroy
     */
    destroy: function(){
        /* fix for infinite loop:
         * If this is called from a layer that extends the FlamingoArcLayer the superclass is
         * that FlamingoArcLayer and this function is called again when this.superclass.function is called
         **/
        if (this.superclass.$className == "viewer.viewercontroller.flamingo.FlamingoArcLayer"){
            this.superclass.superclass.destroy.call(this);
        }else{
            this.superclass.destroy.call(this);
        }
    }

});<commit_msg>check for null value of this.map in flamingo layer reload function.<commit_after>/** The FlamingLayer Class **/

/**
 * @constructor
 * @augments Layer
 * @description The superclass for all flamingolayers
 * @param id The id of the layer
 * @param options The options to be given to the layer
 * @param flamingoObject The flamingo object of the layer
 *
 */

Ext.define("viewer.viewercontroller.flamingo.FlamingoLayer",{
    enabledEvents: null,
    type: null,
    constructor :function (config){
        if (config.id){
            this.id = config.viewerController.mapComponent.makeFlamingoAcceptableId(config.id);
            config.id = this.id;
        }
        this.enabledEvents=new Object();
        return this;
    },
    /**
     * Get's the frameworklayer: the viewer specific layer.
     */
    getFrameworkLayer : function(){
        if (this.map==null){
            return null;
        }
        return this.map.getFrameworkMap();
    },

    toXML : function(){
        Ext.Error.raise({msg: "FlamingoLayer.toXML(): .toXML() must be made!"});
    },

    getTagName : function(){
        Ext.Error.raise({msg: "FlamingoLayer.getTagName: .getTagName() must be made!"});
    },
    setOption : function(optionKey,optionValue){
        this.options[optionKey]=optionValue;
    },
    getId : function(){
        return this.id;
    },
    getFrameworkId: function(){
        return this.map.getId()+"_"+this.getId();
    },
    setAlpha : function (alpha){
        // Fixup wrong Flash alpha, it thinks 0 is transparent and 100 is opaque
        if(this.map) {
            this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"setAlpha",alpha)
        }
    },
    getAlpha : function (){
        return this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"getAlpha");
    },
    /**
     * Get the last getMap request array
     * @see viewer.viewerController.controller.Layer#getLastMapRequest
     */
    getLastMapRequest: function(){
        var request=this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"getLastGetMapRequest");
        if (request==null){
            return null;
        }
        return [request];
    },
    /**
     * Returns the type of the layer.
     */
    getType: function(){
        return this.type;
    },
    /**
     * reloads the framework map
     */
    reload: function(){
        if (this.map !=null && this.map.getFrameworkMap()){
            return this.map.getFrameworkMap().callMethod(this.getFrameworkId(),"update");
        }
    },
    /**
     * Overwrites the addListener function. Add's the event to allowexternalinterface of flamingo
     * so flamingo is allowed to broadcast the event.
     */
    addListener : function(event,handler,scope){
        //enable flamingo event broadcasting
        var flamEvent=this.map.mapComponent.eventList[event];
        if (flamEvent!=undefined){
            //if not enabled yet, add it
            if (this.enabledEvents[flamEvent]==undefined){
               this.map.getFrameworkMap().callMethod(this.map.mapComponent.getId(),"addAllowExternalInterface",this.getFrameworkId()+"."+flamEvent);
               this.enabledEvents[flamEvent]=true;
            }
        }
        /* fix for infinite loop:
         * If this is called from a layer that extends the FlamingoArcLayer the superclass is
         * that FlamingoArcLayer and this function is called again when this.superclass.function is called
         **/
        if (this.superclass.$className == "viewer.viewercontroller.flamingo.FlamingoArcLayer"){
            this.superclass.superclass.addListener.call(this,event,handler,scope);
        }else{
            this.superclass.addListener.call(this,event,handler,scope);
        }
        //viewer.viewercontroller.controller.Layer.superclass.addListener.call(this,event,handler,scope);
    },
    setVisible : function (visible){
        this.visible = visible;

        if (this.options!=null){
            this.options["visible"] = visible;
        }

        if (this.map !=null){
            this.map.getFrameworkMap().callMethod(this.map.id + "_" + this.id, "setVisible", visible);
        }
    },
    /**
     * Get the visibility
     */
    getVisible : function (){
        if (this.map !=null){
            this.visible = this.map.getFrameworkMap().callMethod(this.map.id + "_" + this.id, "getVisible");
        }
        if (this.options!=null)
            this.options["visible"] = this.visible;
        return this.visible;
    },
    /**
     * Overwrite destroy, clear Listeners and forward to super.destroy
     */
    destroy: function(){
        /* fix for infinite loop:
         * If this is called from a layer that extends the FlamingoArcLayer the superclass is
         * that FlamingoArcLayer and this function is called again when this.superclass.function is called
         **/
        if (this.superclass.$className == "viewer.viewercontroller.flamingo.FlamingoArcLayer"){
            this.superclass.superclass.destroy.call(this);
        }else{
            this.superclass.destroy.call(this);
        }
    }

});<|endoftext|>


<commit_before>var Plugin = require('./plugin'),
    is = require('../is'),
    obj = require('../object'),
    arr = require('../array'),
    aEach = arr.each,
    mix = obj.mix,
    apply = obj.apply;

function PluginManager() {}

PluginManager.prototype = {
    defaultPlugin: Plugin,

    init: function(instanceValues) {
        apply(this, instanceValues);
        this.createPlugins();
    },

    createPlugins: function() {
        var config = this.instanceArgs[0];

        this.plugins = [];

        aEach(config.plugins, function(pluginConfig) {
            pluginConfig.owner = this.instance;
            var pluginInstance = this.createPlugin(pluginConfig);

            this.initPlugin(pluginInstance);

            this.plugins.push(pluginInstance);
        }, this);
    },

    createPlugin: function(config) {

        if (config.Constructor) {
            //call the configed Constructor with the
            //passed in config but take off the Constructor
            //config.

            //The plugin Constructor
            //should not need to know about itself
            return new config.Constructor(apply(config, {
                Constructor: undefined
            }));
        }

        //if Constructor property is not on
        //the config just use the default Plugin
        return new this.defaultPlugin(config);
    },

    initPlugin: function(plugin) {
        if (is.isFunction(plugin.init)) {
            plugin.init(this.instance);
        }
    },

    destroyPlugins: function() {
        this.plugins.forEach(function(plugin) {
            if (is.isFunction(plugin.destroy)) {
                plugin.destroy(this.instance);
            }
        });
    }
};

module.exports = PluginManager;<commit_msg>add more methods to pluginManager<commit_after>var Plugin = require('./plugin'),
    is = require('../is'),
    obj = require('../object'),
    arr = require('../array'),
    removeFirst = arr.removeFirst,
    aEach = arr.each,
    mix = obj.mix,
    apply = obj.apply;

function PluginManager() {}

PluginManager.prototype = {
    defaultPlugin: Plugin,

    init: function(instanceValues) {
        apply(this, instanceValues);
        this.plugins = [];
        this.createPlugins();
    },

    createPlugins: function() {
        aEach(this.getPluginConfigFromInstance(), function(pluginConfig) {
            this.addPlugin(pluginConfig);
        }, this);
    },

    getPluginConfigFromInstance: function() {
        var config = this.instanceArgs[0];
        return config.plugins;
    },

    addPlugin: function(pluginConfig) {
        var pluginInstance = this.createPlugin(pluginConfig);

        this.initPlugin(pluginInstance);

        this.plugins.push(pluginInstance);
    },

    createPlugin: function(config) {
        config.owner = this.instance;

        if (config.Constructor) {
            //call the configed Constructor with the
            //passed in config but take off the Constructor
            //config.

            //The plugin Constructor
            //should not need to know about itself
            return new config.Constructor(apply(config, {
                Constructor: undefined
            }));
        }

        //if Constructor property is not on
        //the config just use the default Plugin
        return new this.defaultPlugin(config);
    },

    initPlugin: function(plugin) {
        if (is.isFunction(plugin.init)) {
            plugin.init(this.instance);
        }
    },

    destroyPlugin: function(plugin) {
        if (is.isFunction(plugin.destroy)) {
            plugin.destroy(this.instance);
        }
    },

    destroyPlugins: function() {
        this.plugins.forEach(function(plugin) {
            this.destroyPlugin(plugin);
        }, this);
    },

    /**
     * Remove the plugin from the plugins array and
     * if found destroy it.
     * @param  {Object} object to use to match
     * the plugin to remove.
     * @return {Objec}
     */
    removePlugin: function(obj) {
        var plugin = removeFirst(this.plugins, obj);
        if(plugin) {
            this.destroyPlugin(plugin);
        }

        return plugin;
    }
};

module.exports = PluginManager;<|endoftext|>