<commit_before>(function (mt, undefined) {
    'use strict';

    // add an event property "redundantClick" to tell that if the click was done quickly after the first one
    // useful to filter out inadvertent in some scenario.
    mt.MixTubeApp.directive('ngClick', function () {
        return {
            restrict: 'A',
            priority: 10, // priority at 10 to be executed before the original angular click directive
            link: function (scope, element) {
                element.bind('click.mtLastClick', function (evt) {
                    var now = Date.now();
                    var lastClickTs = element.data('mtLastClickTS');
                    if (lastClickTs && now - lastClickTs < 500) {
                        evt.redundantClick = true;
                    }
                    element.data('mtLastClickTS', now);
                });
            }
        };
    });

    // simple event listener directives for focus and blur events type
    ['blur', 'focus'].forEach(function (evtName) {
        var directiveName = 'mt' + mt.tools.capitalize(evtName);
        mt.MixTubeApp.directive(directiveName, function ($parse) {
            return function (scope, elmt, attr) {
                var fn = $parse(attr[directiveName]);
                elmt.bind(evtName, function (event) {
                    scope.$apply(function () {
                        fn(scope, {$event: event});
                    });
                });
            };
        });
    });

    // intercept rendering initiated by ngModel directive in order to focus the element on model change
    // it is useful to enforce good sequencing of focus then value affectation to get a proper caret position (FF and IE)
    mt.MixTubeApp.directive('mtFocusOnRender', function ($timeout) {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, element, attr, ngModelCtrl) {
                var saved$render = ngModelCtrl.$render;
                ngModelCtrl.$render = function () {
                    // we need the input to be visible before set the value (and by focusing it thanks to model change)
                    // the only way from now is to protectively defer the affectation thanks to timeout
                    // for details see https://github.com/angular/angular.js/issues/1250#issuecomment-8604033
                    $timeout(function () {
                        element.focus();
                        saved$render();
                        if (attr.hasOwnProperty('autoSelect')) {
                            element[0].select();
                        }
                    }, 0);
                };
            }
        };
    });

    // mouse start and stop directive which are mouse move listener with debouncing for respectively leading and
    // trailing edge of the wait period defined by the related attribute "debounce"
    [
        {name: 'start', debounceParams: {leading: true, trailing: false}},
        {name: 'stop', debounceParams: {leading: false, trailing: true}}
    ].forEach(function (descriptor) {
            var directiveName = 'mtMouse' + descriptor.name;

            mt.MixTubeApp.directive(directiveName, function ($parse) {
                return function (scope, elmt, attr) {

                    var fn = $parse(attr[directiveName]);

                    // get the debounce wait value (default value if unspecified is 500ms)
                    var waitTime = parseInt(attr.debounce, 10) || 500;

                    elmt.bind('mousemove', _.debounce(function (evt) {
                        scope.$apply(function () {
                            fn(scope, {$event: evt});
                        });
                    }, waitTime, descriptor.debounceParams));
                };
            });
        });

    mt.MixTubeApp.directive('mtScrollview', function () {

        /**
         * Return the given element ClientRect including the margins.
         *
         * @param {jQuery} $elem
         * @returns {ClientRect}
         */
        function getWholeClientRect($elem) {
            var rect = angular.copy($elem[0].getBoundingClientRect());
            rect.top -= parseFloat($elem.css('margin-top'));
            rect.bottom += parseFloat($elem.css('margin-bottom'));
            rect.left -= parseFloat($elem.css('margin-left'));
            rect.rigth += parseFloat($elem.css('margin-right'));
            return rect;
        }

        return {
            restrict: 'E',
            replace: true,
            transclude: true,
            template: '

',
            controller: function ($element) {
                var scrollViewRect = $element[0].getBoundingClientRect();

                this.scrollIntoView = function ($targetElement) {
                    var targetRect = getWholeClientRect($targetElement);
                    $element.animate({scrollTop: targetRect.top - scrollViewRect.top}, 'fast');
                };
            }
        };
    });

    var scrollIntoViewIfName = 'mtScrollIntoViewIf';
    mt.MixTubeApp.directive(scrollIntoViewIfName, function ($timeout) {
        return {
            restrict: 'A',
            require: '^mtScrollview',
            link: function (scope, element, attrs, mtScrollviewCtrl) {
                scope.$watch(attrs[scrollIntoViewIfName], function (value) {
                    if (value) {
                        $timeout(function () {
                            mtScrollviewCtrl.scrollIntoView(element);
                        }, 0);
                    }
                });
            }
        };
    });

    mt.MixTubeApp.directive('mtInlineEdit', function ($templateCache, $compile, $parse, mtKeyboardShortcutManager) {
        return {
            restrict: 'A',
            link: function (scope, element, attr) {
                var inputFacetLinker = $compile($templateCache.get('mtInlineEditTemplate').trim());

                var mtInlineEditGet = $parse(attr.mtInlineEdit);
                var mtInlineEditSet = mtInlineEditGet.assign;

                // the insulated scope allows to choose when we want to send the updates to the parent model
                // we can't use a directive wide insulated scope because the display element may need some properties
                // from the parent scope to render
                var inputScope = scope.$new(true);
                inputScope.edition = false;
                inputScope.model = null;
                inputScope.blur = function () {
                    rollback();
                };

                function save() {
                    inputScope.edition = false;
                    mtInlineEditSet(scope, inputScope.model);
                    inputScope.model = null;
                }

                function rollback() {
                    inputScope.edition = false;
                    inputScope.model = null;
                }

                inputFacetLinker(inputScope, function (inputFacet) {
                    // register keyboard shortcut context for the input element
                    var keyboardShortcutContext = 'mtInlineEdit_' + inputScope.$id;
                    mtKeyboardShortcutManager.register(keyboardShortcutContext, 'return', function () {
                        save();
                    });
                    mtKeyboardShortcutManager.register(keyboardShortcutContext, 'esc', function () {
                        rollback();
                    });

                    // place the input just after de display element
                    element.after(inputFacet);

                    inputScope.$watch('edition', function (newEdition, oldEdition) {
                        if (newEdition !== oldEdition) {
                            if (newEdition) {
                                element.hide();
                                mtKeyboardShortcutManager.enterContext(keyboardShortcutContext);
                            } else {
                                mtKeyboardShortcutManager.leaveContext(keyboardShortcutContext);
                                element.show();
                            }
                        }
                    });

                    element.bind('click', function () {
                        inputScope.$apply(function () {
                            inputScope.edition = true;
                            inputScope.model = mtInlineEditGet(scope);
                        });
                    });
                });
            }
        };
    });

    mt.MixTubeApp.directive('mtCarousel', function ($rootScope, $window, $timeout) {

        var CAROUSEL_EXPRESSION_REGEXP = /^\s*(.+)\s+in\s+(.*?)\s*$/;

        /**
         * @param {string} expression
         * @returns {{valueIdentifier: string, listIdentifier: string}}
         */
        function parseRepeatExpression(expression) {
            var match = expression.match(CAROUSEL_EXPRESSION_REGEXP);
            if (!match) {
                throw new Error('Expected itemRepeat in form of "_item_ in _array_" but got "' + expression + '".');
            }
            return {
                valueIdentifier: match[1],
                listIdentifier: match[2]
            };
        }

        /**
         * @param {jQuery} target
         * @param {function()} callback
         * @returns {{disconnect: function()}}
         */
        function observeChildList(target, callback) {
            if (window.hasOwnProperty('MutationObserver')) {
                return new MutationObserver(function () {
                    $rootScope.$apply(function () {
                        callback();
                    });
                }).observe(target[0], { childList: true });
            } else {
                // only IE10 in the range of supported browsers
                // todo remove once IE11 has been released
                target.bind('DOMNodeInserted.mtCarousel DOMNodeRemoved.mtCarousel', function () {
                    // we need to let time for the node to be rendered before calling the callback
                    $timeout(function () {
                        callback();
                    }, 0);
                });

                return {
                    disconnect: function () {
                        target.unbind('DOMNodeInserted.mtCarousel DOMNodeRemoved.mtCarousel');
                    }
                };
            }
        }

        return {
            restrict: 'E',
            replace: true,
            transclude: 'element',
            scope: true,
            template: function (tElement, tAttr) {
                return '
' +
                    '
' +
                    '
' +
                    '

' +
                    '

' +
                    '
' +
                    '
' +
                    '
';
            },
            controller: function ($scope, $element) {

                var self = this;
                var carousel = $element;
                var slider = carousel.find('.mt-carousel-slider');
                var savedList = [];

                $scope.backwardAvailable = false;
                $scope.forwardAvailable = false;

                /**
                 * Pick the best carousel bucket available around the given x position.
                 *
                 * @param {number} x the position
                 * @returns {HTMLElement} the item at the position or undefined if none found
                 */
                function rawBucketFromPosition(x) {
                    return _.findWhere(carousel.find('.mt-carousel-bucket'), function (bucket) {
                        var bucketRect = bucket.getBoundingClientRect();
                        if (x > 0) {
                            // in forward we want the half visible bucket at the very right
                            // means the first bucket where the right edge position is higher that the looked for position
                            return x < bucketRect.right;
                        } else {
                            // in backward we want the fully visible bucket at 1 view port width distance from the left
                            // means the first bucket where the left edge position is higher that the looked for position
                            return x < bucketRect.left;
                        }
                    });
                }

                function page(forward) {
                    // works because the carousel is full width
                    var toBringUp = rawBucketFromPosition(carousel[0].getBoundingClientRect().width * (forward ? 1 : -1));
                    if (toBringUp) {
                        self.bringUp(angular.element(toBringUp));
                    }
                }

                function computeHandlesAvailability() {
                    var sliderRect = slider[0].getBoundingClientRect();
                    var carouselRect = carousel[0].getBoundingClientRect();

                    $scope.backwardAvailable = sliderRect.left < carouselRect.left;
                    $scope.forwardAvailable = carouselRect.right < sliderRect.right;
                }

                self.computeSizeRelated = function () {
                    computeHandlesAvailability();
                };

                self.bucketsUpdated = function (newList) {
                    if (angular.isArray(newList)) {
                        if (savedList.length < newList.length) {
                            // some buckets were added, we want to detect the index of the first one
                            // bellow the proper way to do it but because we now that we only add bucket at then

                            var addedBucketIndex = -1;
                            for (var idx = 0; idx < savedList.length; idx++) {
                                if (savedList[idx] !== newList[idx]) {
                                    // found a different, save the index and break
                                    addedBucketIndex = idx;
                                    break;
                                }
                            }
                            if (addedBucketIndex === -1) {
                                // unsuccessful search means the new bucket is in the not yet explored part of the new list
                                addedBucketIndex = savedList.length;
                            }

                            // the bucket has been just added bring it up
                            self.bringUp(carousel.find('.mt-carousel-bucket').eq(addedBucketIndex));
                        }

                        // shallow copy the list for next change detection
                        savedList = newList.slice();
                    }
                };

                self.bringUp = function (toBringUp) {
                    var viewPortRect = carousel[0].getBoundingClientRect();
                    var toBringUpRect = toBringUp[0].getBoundingClientRect();

                    if (toBringUpRect.left < viewPortRect.left || viewPortRect.right < toBringUpRect.right) {
                        // the element to bring up is outside of the view port
                        // we want to make it the first visible item in the view port
                        var sliderRect = slider[0].getBoundingClientRect();
                        var newPosition = sliderRect.left - toBringUpRect.left;

                        slider.animate({left: newPosition}, function () {
                            $scope.$apply(function () {
                                computeHandlesAvailability();
                            })
                        });
                    }
                };

                self.backward = function () {
                    page(false);
                };

                self.forward = function () {
                    page(true);
                };
            },
            compile: function (tElement, tAttr, originalLinker) {
                // we get the original directive outer HTML by executing the linker on a empty scope
                var tOriginal = originalLinker($rootScope.$new(true));
                var tHandles = tOriginal.find('handle');
                var tRenderer = tOriginal.find('renderer');
                var tRemainder = tOriginal.find('remainder');

                tElement.append(tHandles.contents());
                tElement.find('.js-mt-carousel-item-bucket').append(tRenderer.contents());
                tElement.find('.js-mt-carousel-remainder-bucket').append(tRemainder.contents());

                return function link(scope, element, attr, carouselCtrl) {
                    // react to items list changes
                    var identifiers = parseRepeatExpression(attr.bucketsRepeat);

                    scope.$watchCollection(identifiers.listIdentifier, function (newList) {
                        // works only with list of bucket
                        carouselCtrl.bucketsUpdated(newList);
                    });

                    // react to window resizing
                    var window = angular.element($window);
                    window.bind('resize.mtCarousel', _.debounce(function () {
                        scope.$apply(function () {
                            carouselCtrl.computeSizeRelated();
                        });
                    }, 100));

                    // react to bucket insertion/removal
                    var bucketListObserver = observeChildList(element.find('.mt-carousel-list'), function () {
                        carouselCtrl.computeSizeRelated();
                    });

                    scope.$on('$destroy', function () {
                        window.unbind('resize.mtCarousel');
                        bucketListObserver.disconnect();
                    });

                    // methods that can be used by sub components
                    scope.backward = function () {
                        carouselCtrl.backward();
                    };
                    scope.forward = function () {
                        carouselCtrl.forward();
                    };
                }
            }
        };
    });

    mt.MixTubeApp.directive('mtInternalBringBucketUpWhen', function () {
        return {
            restrict: 'A',
            require: '^mtCarousel',
            link: function (scope, element, attrs, carouselCtrl) {
                scope.$watch(attrs.mtInternalBringBucketUpWhen, function watchBringUpIf(bringUp) {
                    if (bringUp) {
                        carouselCtrl.bringUp(element);
                    }
                });
            }
        };
    });

    // a duration formatter that takes a duration in milliseconds and returns a formatted duration like "h:mm"
    mt.MixTubeApp.filter('mtDuration', function () {
        // reuse the date object between invocation since it is only used as a formatting tool
        var singletonDate = new Date(0, 0, 0, 0, 0, 0, 0);
        // time that represent the absolute zero for date, all fields to zero
        // needs to be computed because of timezones differences
        var absoluteDateZero = singletonDate.getTime();

        return function (time) {
            if (isNaN(time)) {
                return '';
            }

            // reset the time to the zero to calculate durations
            singletonDate.setTime(absoluteDateZero);
            singletonDate.setMilliseconds(time);

            return (singletonDate.getHours() * 60 + singletonDate.getMinutes()).toString(10) + ':' + mt.tools.leftPad(singletonDate.getSeconds().toString(10), 2, '0');
        }
    });
})(mt);<commit_msg>carousel : replace js animation by transitions<commit_after>(function (mt, undefined) {
    'use strict';

    // add an event property "redundantClick" to tell that if the click was done quickly after the first one
    // useful to filter out inadvertent in some scenario.
    mt.MixTubeApp.directive('ngClick', function () {
        return {
            restrict: 'A',
            priority: 10, // priority at 10 to be executed before the original angular click directive
            link: function (scope, element) {
                element.bind('click.mtLastClick', function (evt) {
                    var now = Date.now();
                    var lastClickTs = element.data('mtLastClickTS');
                    if (lastClickTs && now - lastClickTs < 500) {
                        evt.redundantClick = true;
                    }
                    element.data('mtLastClickTS', now);
                });
            }
        };
    });

    // simple event listener directives for focus and blur events type
    ['blur', 'focus'].forEach(function (evtName) {
        var directiveName = 'mt' + mt.tools.capitalize(evtName);
        mt.MixTubeApp.directive(directiveName, function ($parse) {
            return function (scope, elmt, attr) {
                var fn = $parse(attr[directiveName]);
                elmt.bind(evtName, function (event) {
                    scope.$apply(function () {
                        fn(scope, {$event: event});
                    });
                });
            };
        });
    });

    // intercept rendering initiated by ngModel directive in order to focus the element on model change
    // it is useful to enforce good sequencing of focus then value affectation to get a proper caret position (FF and IE)
    mt.MixTubeApp.directive('mtFocusOnRender', function ($timeout) {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, element, attr, ngModelCtrl) {
                var saved$render = ngModelCtrl.$render;
                ngModelCtrl.$render = function () {
                    // we need the input to be visible before set the value (and by focusing it thanks to model change)
                    // the only way from now is to protectively defer the affectation thanks to timeout
                    // for details see https://github.com/angular/angular.js/issues/1250#issuecomment-8604033
                    $timeout(function () {
                        element.focus();
                        saved$render();
                        if (attr.hasOwnProperty('autoSelect')) {
                            element[0].select();
                        }
                    }, 0);
                };
            }
        };
    });

    // mouse start and stop directive which are mouse move listener with debouncing for respectively leading and
    // trailing edge of the wait period defined by the related attribute "debounce"
    [
        {name: 'start', debounceParams: {leading: true, trailing: false}},
        {name: 'stop', debounceParams: {leading: false, trailing: true}}
    ].forEach(function (descriptor) {
            var directiveName = 'mtMouse' + descriptor.name;

            mt.MixTubeApp.directive(directiveName, function ($parse) {
                return function (scope, elmt, attr) {

                    var fn = $parse(attr[directiveName]);

                    // get the debounce wait value (default value if unspecified is 500ms)
                    var waitTime = parseInt(attr.debounce, 10) || 500;

                    elmt.bind('mousemove', _.debounce(function (evt) {
                        scope.$apply(function () {
                            fn(scope, {$event: evt});
                        });
                    }, waitTime, descriptor.debounceParams));
                };
            });
        });

    mt.MixTubeApp.directive('mtScrollview', function () {

        /**
         * Return the given element ClientRect including the margins.
         *
         * @param {jQuery} $elem
         * @returns {ClientRect}
         */
        function getWholeClientRect($elem) {
            var rect = angular.copy($elem[0].getBoundingClientRect());
            rect.top -= parseFloat($elem.css('margin-top'));
            rect.bottom += parseFloat($elem.css('margin-bottom'));
            rect.left -= parseFloat($elem.css('margin-left'));
            rect.rigth += parseFloat($elem.css('margin-right'));
            return rect;
        }

        return {
            restrict: 'E',
            replace: true,
            transclude: true,
            template: '

',
            controller: function ($element) {
                var scrollViewRect = $element[0].getBoundingClientRect();

                this.scrollIntoView = function ($targetElement) {
                    var targetRect = getWholeClientRect($targetElement);
                    $element.animate({scrollTop: targetRect.top - scrollViewRect.top}, 'fast');
                };
            }
        };
    });

    var scrollIntoViewIfName = 'mtScrollIntoViewIf';
    mt.MixTubeApp.directive(scrollIntoViewIfName, function ($timeout) {
        return {
            restrict: 'A',
            require: '^mtScrollview',
            link: function (scope, element, attrs, mtScrollviewCtrl) {
                scope.$watch(attrs[scrollIntoViewIfName], function (value) {
                    if (value) {
                        $timeout(function () {
                            mtScrollviewCtrl.scrollIntoView(element);
                        }, 0);
                    }
                });
            }
        };
    });

    mt.MixTubeApp.directive('mtInlineEdit', function ($templateCache, $compile, $parse, mtKeyboardShortcutManager) {
        return {
            restrict: 'A',
            link: function (scope, element, attr) {
                var inputFacetLinker = $compile($templateCache.get('mtInlineEditTemplate').trim());

                var mtInlineEditGet = $parse(attr.mtInlineEdit);
                var mtInlineEditSet = mtInlineEditGet.assign;

                // the insulated scope allows to choose when we want to send the updates to the parent model
                // we can't use a directive wide insulated scope because the display element may need some properties
                // from the parent scope to render
                var inputScope = scope.$new(true);
                inputScope.edition = false;
                inputScope.model = null;
                inputScope.blur = function () {
                    rollback();
                };

                function save() {
                    inputScope.edition = false;
                    mtInlineEditSet(scope, inputScope.model);
                    inputScope.model = null;
                }

                function rollback() {
                    inputScope.edition = false;
                    inputScope.model = null;
                }

                inputFacetLinker(inputScope, function (inputFacet) {
                    // register keyboard shortcut context for the input element
                    var keyboardShortcutContext = 'mtInlineEdit_' + inputScope.$id;
                    mtKeyboardShortcutManager.register(keyboardShortcutContext, 'return', function () {
                        save();
                    });
                    mtKeyboardShortcutManager.register(keyboardShortcutContext, 'esc', function () {
                        rollback();
                    });

                    // place the input just after de display element
                    element.after(inputFacet);

                    inputScope.$watch('edition', function (newEdition, oldEdition) {
                        if (newEdition !== oldEdition) {
                            if (newEdition) {
                                element.hide();
                                mtKeyboardShortcutManager.enterContext(keyboardShortcutContext);
                            } else {
                                mtKeyboardShortcutManager.leaveContext(keyboardShortcutContext);
                                element.show();
                            }
                        }
                    });

                    element.bind('click', function () {
                        inputScope.$apply(function () {
                            inputScope.edition = true;
                            inputScope.model = mtInlineEditGet(scope);
                        });
                    });
                });
            }
        };
    });

    mt.MixTubeApp.directive('mtCarousel', function ($rootScope, $window, $timeout) {

        var CAROUSEL_EXPRESSION_REGEXP = /^\s*(.+)\s+in\s+(.*?)\s*$/;
        var EASE_IN_OUT_QUART = 'cubic-bezier(.77,0,.175,1)';

        /**
         * @param {string} expression
         * @returns {{valueIdentifier: string, listIdentifier: string}}
         */
        function parseRepeatExpression(expression) {
            var match = expression.match(CAROUSEL_EXPRESSION_REGEXP);
            if (!match) {
                throw new Error('Expected itemRepeat in form of "_item_ in _array_" but got "' + expression + '".');
            }
            return {
                valueIdentifier: match[1],
                listIdentifier: match[2]
            };
        }

        /**
         * @param {jQuery} target
         * @param {function()} callback
         * @returns {{disconnect: function()}}
         */
        function observeChildList(target, callback) {
            if (window.hasOwnProperty('MutationObserver')) {
                return new MutationObserver(function () {
                    $rootScope.$apply(function () {
                        callback();
                    });
                }).observe(target[0], { childList: true });
            } else {
                // only IE10 in the range of supported browsers
                // todo remove once IE11 has been released
                target.bind('DOMNodeInserted.mtCarousel DOMNodeRemoved.mtCarousel', function () {
                    // we need to let time for the node to be rendered before calling the callback
                    $timeout(function () {
                        callback();
                    }, 0);
                });

                return {
                    disconnect: function () {
                        target.unbind('DOMNodeInserted.mtCarousel DOMNodeRemoved.mtCarousel');
                    }
                };
            }
        }

        return {
            restrict: 'E',
            replace: true,
            transclude: 'element',
            scope: true,
            template: function (tElement, tAttr) {
                return '
' +
                    '
' +
                    '
' +
                    '

' +
                    '

' +
                    '
' +
                    '
' +
                    '
';
            },
            controller: function ($scope, $element) {

                var self = this;
                var carousel = $element;
                var slider = carousel.find('.mt-carousel-slider');
                var savedList = [];

                // allows to animate the slider
                slider.css('transition', 'all .5s ' + EASE_IN_OUT_QUART);

                $scope.backwardAvailable = false;
                $scope.forwardAvailable = false;

                /**
                 * Pick the best carousel bucket available around the given x position.
                 *
                 * @param {number} x the position
                 * @returns {HTMLElement} the item at the position or undefined if none found
                 */
                function rawBucketFromPosition(x) {
                    return _.findWhere(carousel.find('.mt-carousel-bucket'), function (bucket) {
                        var bucketRect = bucket.getBoundingClientRect();
                        if (x > 0) {
                            // in forward we want the half visible bucket at the very right
                            // means the first bucket where the right edge position is higher that the looked for position
                            return x < bucketRect.right;
                        } else {
                            // in backward we want the fully visible bucket at 1 view port width distance from the left
                            // means the first bucket where the left edge position is higher that the looked for position
                            return x < bucketRect.left;
                        }
                    });
                }

                function page(forward) {
                    // works because the carousel is full width
                    var toBringUp = rawBucketFromPosition(carousel[0].getBoundingClientRect().width * (forward ? 1 : -1));
                    if (toBringUp) {
                        self.bringUp(angular.element(toBringUp));
                    }
                }

                function computeHandlesAvailability() {
                    var sliderRect = slider[0].getBoundingClientRect();
                    var carouselRect = carousel[0].getBoundingClientRect();

                    $scope.backwardAvailable = sliderRect.left < carouselRect.left;
                    $scope.forwardAvailable = carouselRect.right < sliderRect.right;
                }

                self.computeSizeRelated = function () {
                    computeHandlesAvailability();
                };

                self.bucketsUpdated = function (newList) {
                    if (angular.isArray(newList)) {
                        if (savedList.length < newList.length) {
                            // some buckets were added, we want to detect the index of the first one
                            // bellow the proper way to do it but because we now that we only add bucket at then

                            var addedBucketIndex = -1;
                            for (var idx = 0; idx < savedList.length; idx++) {
                                if (savedList[idx] !== newList[idx]) {
                                    // found a different, save the index and break
                                    addedBucketIndex = idx;
                                    break;
                                }
                            }
                            if (addedBucketIndex === -1) {
                                // unsuccessful search means the new bucket is in the not yet explored part of the new list
                                addedBucketIndex = savedList.length;
                            }

                            // the bucket has been just added bring it up
                            self.bringUp(carousel.find('.mt-carousel-bucket').eq(addedBucketIndex));
                        }

                        // shallow copy the list for next change detection
                        savedList = newList.slice();
                    }
                };

                self.bringUp = function (toBringUp) {
                    var viewPortRect = carousel[0].getBoundingClientRect();
                    var toBringUpRect = toBringUp[0].getBoundingClientRect();

                    if (toBringUpRect.left < viewPortRect.left || viewPortRect.right < toBringUpRect.right) {
                        // the element to bring up is outside of the view port
                        // we want to make it the first visible item in the view port
                        var sliderRect = slider[0].getBoundingClientRect();
                        var newPosition = sliderRect.left - toBringUpRect.left;

                        slider.css('transform', 'translateX(' + newPosition + 'px)');
                        // listen for end of transition to compute handles availability
                        slider.one('MSTransitionEnd.mtCarousel transitionend.mtCarousel', function (evt) {
                            $scope.$apply(function () {
                                computeHandlesAvailability();
                            });
                        });

                    }
                };

                self.backward = function () {
                    page(false);
                };

                self.forward = function () {
                    page(true);
                };
            },
            compile: function (tElement, tAttr, originalLinker) {
                // we get the original directive outer HTML by executing the linker on a empty scope
                var tOriginal = originalLinker($rootScope.$new(true));
                var tHandles = tOriginal.find('handle');
                var tRenderer = tOriginal.find('renderer');
                var tRemainder = tOriginal.find('remainder');

                tElement.append(tHandles.contents());
                tElement.find('.js-mt-carousel-item-bucket').append(tRenderer.contents());
                tElement.find('.js-mt-carousel-remainder-bucket').append(tRemainder.contents());

                return function link(scope, element, attr, carouselCtrl) {
                    // react to items list changes
                    var identifiers = parseRepeatExpression(attr.bucketsRepeat);

                    scope.$watchCollection(identifiers.listIdentifier, function (newList) {
                        // works only with list of bucket
                        carouselCtrl.bucketsUpdated(newList);
                    });

                    // react to window resizing
                    var window = angular.element($window);
                    window.bind('resize.mtCarousel', _.debounce(function () {
                        scope.$apply(function () {
                            carouselCtrl.computeSizeRelated();
                        });
                    }, 100));

                    // react to bucket insertion/removal
                    var bucketListObserver = observeChildList(element.find('.mt-carousel-list'), function () {
                        carouselCtrl.computeSizeRelated();
                    });

                    scope.$on('$destroy', function () {
                        window.unbind('resize.mtCarousel');
                        bucketListObserver.disconnect();
                    });

                    // methods that can be used by sub components
                    scope.backward = function () {
                        carouselCtrl.backward();
                    };
                    scope.forward = function () {
                        carouselCtrl.forward();
                    };
                }
            }
        };
    });

    mt.MixTubeApp.directive('mtInternalBringBucketUpWhen', function () {
        return {
            restrict: 'A',
            require: '^mtCarousel',
            link: function (scope, element, attrs, carouselCtrl) {
                scope.$watch(attrs.mtInternalBringBucketUpWhen, function watchBringUpIf(bringUp) {
                    if (bringUp) {
                        carouselCtrl.bringUp(element);
                    }
                });
            }
        };
    });

    // a duration formatter that takes a duration in milliseconds and returns a formatted duration like "h:mm"
    mt.MixTubeApp.filter('mtDuration', function () {
        // reuse the date object between invocation since it is only used as a formatting tool
        var singletonDate = new Date(0, 0, 0, 0, 0, 0, 0);
        // time that represent the absolute zero for date, all fields to zero
        // needs to be computed because of timezones differences
        var absoluteDateZero = singletonDate.getTime();

        return function (time) {
            if (isNaN(time)) {
                return '';
            }

            // reset the time to the zero to calculate durations
            singletonDate.setTime(absoluteDateZero);
            singletonDate.setMilliseconds(time);

            return (singletonDate.getHours() * 60 + singletonDate.getMinutes()).toString(10) + ':' + mt.tools.leftPad(singletonDate.getSeconds().toString(10), 2, '0');
        }
    });
})(mt);<|endoftext|>


<commit_before>// ==UserScript==
// @id             ingress-intel-total-conversion@jonatkins
// @name           IITC: Ingress intel map total conversion
// @version        0.16.5.@@DATETIMEVERSION@@
// @namespace      https://github.com/jonatkins/ingress-intel-total-conversion
// @updateURL      @@UPDATEURL@@
// @downloadURL    @@DOWNLOADURL@@
// @description    [@@BUILDNAME@@-@@BUILDDATE@@] Total conversion for the ingress intel map.
// @include        http://www.ingress.com/intel*
// @include        https://www.ingress.com/intel*
// @match          http://www.ingress.com/intel*
// @match          https://www.ingress.com/intel*
// @grant          none
// ==/UserScript==

// REPLACE ORIG SITE ///////////////////////////////////////////////////
if(document.getElementsByTagName('html')[0].getAttribute('itemscope') != null)
  throw('Ingress Intel Website is down, not a userscript issue.');
window.iitcBuildDate = '@@BUILDDATE@@';

// disable vanilla JS
window.onload = function() {};
document.body.onload = function() {};

// rescue user data from original page
var scr = document.getElementsByTagName('script');
for(var x in scr) {
  var s = scr[x];
  if(s.src) continue;
  if(s.type !== 'text/javascript') continue;
  var d = s.innerHTML.split('\n');
  break;
}

if(!d) {
  // page doesn’t have a script tag with player information.
  if(document.getElementById('header_email')) {
    // however, we are logged in.
    // it used to be regularly common to get temporary 'account not enabled' messages from the intel site.
    // however, this is no longer common. more common is users getting account suspended/banned - and this
    // currently shows the 'not enabled' message. so it's safer to not repeatedly reload in this case
//    setTimeout('location.reload();', 3*1000);
    throw("Page doesn't have player data, but you are logged in.");
  }
  // FIXME: handle nia takedown in progress
  throw("Couldn't retrieve player data. Are you logged in?");
}

for(var i = 0; i < d.length; i++) {
  if(!d[i].match('var PLAYER = ')) continue;
  eval(d[i].match(/^var /, 'window.'));
  break;
}
// player information is now available in a hash like this:
// window.PLAYER = {"ap": "123", "energy": 123, "available_invites": 123, "nickname": "somenick", "team": "ENLIGHTENED||RESISTANCE"};

// remove complete page. We only wanted the user-data and the page’s
// security context so we can access the API easily. Setup as much as
// possible without requiring scripts.
document.getElementsByTagName('head')[0].innerHTML = ''
  + '<title>Ingress Intel Map</title>'
  + ''
  + ''
//note: smartphone.css injection moved into code/smartphone.js
  + '<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Roboto:100,100italic,300,300italic,400,400italic,500,500italic,700,700italic&subset=latin,cyrillic-ext,greek-ext,greek,vietnamese,latin-ext,cyrillic"/>';

document.getElementsByTagName('body')[0].innerHTML = ''
  + '
Loading, please wait
'
  + '
'
  + '  '
  +   'fullcompactpublicfaction'
  + '
'
  + '
'
  + '

'
  + '

'
  + '

'
  + '

'
  + '
'
  + '<form id="chatinput" style="display:none">

'
  + '  <time></time>	'
  + '  tell faction:	'
  + '  <input id="chattext" type="text" maxlength="256" />	'
  + '

</form>'
  + ''
  + '
' // enable scrolling for small screens
  + '
'
  + '
t
'
  + '
 loading global control stats
'
  + '
'
  + '      <input id="geosearch" placeholder="Search location…" type="text"/>'
  + '      '
  + '
'
  + '

'
// redeeming removed from stock site, so commented out for now. it may return...
//  + '    <input id="redeem" placeholder="Redeem code…" type="text"/>'
  + '
'
  + '      Permalink'
  + '      About IITC'
  + '
'
  + '
'
  + '
'
  + '

'
  // avoid error by stock JS
  + '

';

// putting everything in a wrapper function that in turn is placed in a
// script tag on the website allows us to execute in the site’s context
// instead of in the Greasemonkey/Extension/etc. context.
function wrapper(info) {
// a cut-down version of GM_info is passed as a parameter to the script
// (not the full GM_info - it contains the ENTIRE script source!)
window.script_info = info;

// LEAFLET PREFER CANVAS ///////////////////////////////////////////////
// Set to true if Leaflet should draw things using Canvas instead of SVG
// Disabled for now because it has several bugs: flickering, constant
// CPU usage and it continuously fires the moveend event.
L_PREFER_CANVAS = false;

// CONFIG OPTIONS ////////////////////////////////////////////////////
window.REFRESH = 30; // refresh view every 30s (base time)
window.ZOOM_LEVEL_ADJ = 5; // add 5 seconds per zoom level
window.ON_MOVE_REFRESH = 2.5;  //refresh time to use after a movement event
window.MINIMUM_OVERRIDE_REFRESH = 10; //limit on refresh time since previous refresh, limiting repeated move refresh rate
window.REFRESH_GAME_SCORE = 15*60; // refresh game score every 15 minutes
window.MAX_IDLE_TIME = 4*60; // stop updating map after 4min idling
window.PRECACHE_PLAYER_NAMES_ZOOM = 17; // zoom level to start pre-resolving player names
window.HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20;
window.SIDEBAR_WIDTH = 300;
// chat messages are requested for the visible viewport. On high zoom
// levels this gets pretty pointless, so request messages in at least a
// X km radius.
window.CHAT_MIN_RANGE = 6;
// this controls how far data is being drawn outside the viewport. Set
// it 0 to only draw entities that intersect the current view. A value
// of one will render an area twice the size of the viewport (or some-
// thing like that, Leaflet doc isn’t too specific). Setting it too low
// makes the missing data on move/zoom out more obvious. Setting it too
// high causes too many items to be drawn, making drag&drop sluggish.
window.VIEWPORT_PAD_RATIO = 0.3;

// how many items to request each query
window.CHAT_PUBLIC_ITEMS = 50;
window.CHAT_FACTION_ITEMS = 50;
// how many pixels to the top before requesting new data
window.CHAT_REQUEST_SCROLL_TOP = 200;
window.CHAT_SHRINKED = 60;

// Minimum area to zoom ratio that field MU's will display
window.FIELD_MU_DISPLAY_AREA_ZOOM_RATIO = 0.001;

// Point tolerance for displaying MU's
window.FIELD_MU_DISPLAY_POINT_TOLERANCE = 60

window.COLOR_SELECTED_PORTAL = '#f0f';
window.COLORS = ['#FF9900', '#0088FF', '#03DC03']; // none, res, enl
window.COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4'];
window.COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'};

window.MOD_TYPE = {RES_SHIELD:'Shield', MULTIHACK:'Multi-hack', FORCE_AMP:'Force Amp', HEATSINK:'Heat Sink', TURRET:'Turret', LINK_AMPLIFIER: 'Link Amp'};

// circles around a selected portal that show from where you can hack
// it and how far the portal reaches (i.e. how far links may be made
// from this portal)
window.ACCESS_INDICATOR_COLOR = 'orange';
window.RANGE_INDICATOR_COLOR = 'red'

// by how much pixels should the portal range be expanded on mobile
// devices. This should make clicking them easier.
window.PORTAL_RADIUS_ENLARGE_MOBILE = 5;

window.DEFAULT_PORTAL_IMG = '//commondatastorage.googleapis.com/ingress.com/img/default-portal-image.png';
window.NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit=1&q=';

// INGRESS CONSTANTS /////////////////////////////////////////////////
// http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
window.RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000];
window.MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000];
window.MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000];
window.HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it
window.OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE'];
window.OCTANTS_ARROW = ['→', '↗', '↑', '↖', '←', '↙', '↓', '↘'];
window.DESTROY_RESONATOR = 75; //AP for destroying portal
window.DESTROY_LINK = 187; //AP for destroying link
window.DESTROY_FIELD = 750; //AP for destroying field
window.CAPTURE_PORTAL = 500; //AP for capturing a portal
window.DEPLOY_RESONATOR = 125; //AP for deploying a resonator
window.COMPLETION_BONUS = 250; //AP for deploying all resonators on portal
window.UPGRADE_ANOTHERS_RESONATOR = 65; //AP for upgrading another's resonator
window.MAX_PORTAL_LEVEL = 8;
window.MAX_RESO_PER_PLAYER = [0, 8, 4, 4, 4, 2, 2, 1, 1];

// OTHER MORE-OR-LESS CONSTANTS //////////////////////////////////////
window.TEAM_NONE = 0;
window.TEAM_RES = 1;
window.TEAM_ENL = 2;
window.TEAM_TO_CSS = ['none', 'res', 'enl'];

window.SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2];
window.SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2];
window.EARTH_RADIUS=6378137;
window.DEG2RAD = Math.PI / 180;

// STORAGE ///////////////////////////////////////////////////////////
// global variables used for storage. Most likely READ ONLY. Proper
// way would be to encapsulate them in an anonymous function and write
// getters/setters, but if you are careful enough, this works.
window.refreshTimeout = undefined;
window.urlPortal = null;
window.playersToResolve = [];
window.playersInResolving = [];
window.selectedPortal = null;
window.portalRangeIndicator = null;
window.portalAccessIndicator = null;
window.mapRunsUserAction = false;
//var portalsLayers, linksLayer, fieldsLayer;
var portalsFactionLayers, linksFactionLayers, fieldsFactionLayers;

// contain references to all entities loaded from the server. If render limits are hit,
// not all may be added to the leaflet layers
window.portals = {};
window.links = {};
window.fields = {};

window.resonators = {};

// contain current status(on/off) of overlay layerGroups.
// But you should use isLayerGroupDisplayed(name) to check the status
window.overlayStatus = {};

// plugin framework. Plugins may load earlier than iitc, so don’t
// overwrite data
if(typeof window.plugin !== 'function') window.plugin = function() {};

@@INJECTCODE@@

} // end of wrapper

// inject code into site context
var script = document.createElement('script');
var info = { buildName: '@@BUILDNAME@@', dateTimeVersion: '@@DATETIMEVERSION@@' };
if (this.GM_info && this.GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description };
script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');'));
(document.body || document.head || document.documentElement).appendChild(script);
<commit_msg>bump version number<commit_after>// ==UserScript==
// @id             ingress-intel-total-conversion@jonatkins
// @name           IITC: Ingress intel map total conversion
// @version        0.16.6.@@DATETIMEVERSION@@
// @namespace      https://github.com/jonatkins/ingress-intel-total-conversion
// @updateURL      @@UPDATEURL@@
// @downloadURL    @@DOWNLOADURL@@
// @description    [@@BUILDNAME@@-@@BUILDDATE@@] Total conversion for the ingress intel map.
// @include        http://www.ingress.com/intel*
// @include        https://www.ingress.com/intel*
// @match          http://www.ingress.com/intel*
// @match          https://www.ingress.com/intel*
// @grant          none
// ==/UserScript==

// REPLACE ORIG SITE ///////////////////////////////////////////////////
if(document.getElementsByTagName('html')[0].getAttribute('itemscope') != null)
  throw('Ingress Intel Website is down, not a userscript issue.');
window.iitcBuildDate = '@@BUILDDATE@@';

// disable vanilla JS
window.onload = function() {};
document.body.onload = function() {};

// rescue user data from original page
var scr = document.getElementsByTagName('script');
for(var x in scr) {
  var s = scr[x];
  if(s.src) continue;
  if(s.type !== 'text/javascript') continue;
  var d = s.innerHTML.split('\n');
  break;
}

if(!d) {
  // page doesn’t have a script tag with player information.
  if(document.getElementById('header_email')) {
    // however, we are logged in.
    // it used to be regularly common to get temporary 'account not enabled' messages from the intel site.
    // however, this is no longer common. more common is users getting account suspended/banned - and this
    // currently shows the 'not enabled' message. so it's safer to not repeatedly reload in this case
//    setTimeout('location.reload();', 3*1000);
    throw("Page doesn't have player data, but you are logged in.");
  }
  // FIXME: handle nia takedown in progress
  throw("Couldn't retrieve player data. Are you logged in?");
}

for(var i = 0; i < d.length; i++) {
  if(!d[i].match('var PLAYER = ')) continue;
  eval(d[i].match(/^var /, 'window.'));
  break;
}
// player information is now available in a hash like this:
// window.PLAYER = {"ap": "123", "energy": 123, "available_invites": 123, "nickname": "somenick", "team": "ENLIGHTENED||RESISTANCE"};

// remove complete page. We only wanted the user-data and the page’s
// security context so we can access the API easily. Setup as much as
// possible without requiring scripts.
document.getElementsByTagName('head')[0].innerHTML = ''
  + '<title>Ingress Intel Map</title>'
  + ''
  + ''
//note: smartphone.css injection moved into code/smartphone.js
  + '<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Roboto:100,100italic,300,300italic,400,400italic,500,500italic,700,700italic&subset=latin,cyrillic-ext,greek-ext,greek,vietnamese,latin-ext,cyrillic"/>';

document.getElementsByTagName('body')[0].innerHTML = ''
  + '
Loading, please wait
'
  + '
'
  + '  '
  +   'fullcompactpublicfaction'
  + '
'
  + '
'
  + '

'
  + '

'
  + '

'
  + '

'
  + '
'
  + '<form id="chatinput" style="display:none">

'
  + '  <time></time>	'
  + '  tell faction:	'
  + '  <input id="chattext" type="text" maxlength="256" />	'
  + '

</form>'
  + ''
  + '
' // enable scrolling for small screens
  + '
'
  + '
t
'
  + '
 loading global control stats
'
  + '
'
  + '      <input id="geosearch" placeholder="Search location…" type="text"/>'
  + '      '
  + '
'
  + '

'
// redeeming removed from stock site, so commented out for now. it may return...
//  + '    <input id="redeem" placeholder="Redeem code…" type="text"/>'
  + '
'
  + '      Permalink'
  + '      About IITC'
  + '
'
  + '
'
  + '
'
  + '

'
  // avoid error by stock JS
  + '

';

// putting everything in a wrapper function that in turn is placed in a
// script tag on the website allows us to execute in the site’s context
// instead of in the Greasemonkey/Extension/etc. context.
function wrapper(info) {
// a cut-down version of GM_info is passed as a parameter to the script
// (not the full GM_info - it contains the ENTIRE script source!)
window.script_info = info;

// LEAFLET PREFER CANVAS ///////////////////////////////////////////////
// Set to true if Leaflet should draw things using Canvas instead of SVG
// Disabled for now because it has several bugs: flickering, constant
// CPU usage and it continuously fires the moveend event.
L_PREFER_CANVAS = false;

// CONFIG OPTIONS ////////////////////////////////////////////////////
window.REFRESH = 30; // refresh view every 30s (base time)
window.ZOOM_LEVEL_ADJ = 5; // add 5 seconds per zoom level
window.ON_MOVE_REFRESH = 2.5;  //refresh time to use after a movement event
window.MINIMUM_OVERRIDE_REFRESH = 10; //limit on refresh time since previous refresh, limiting repeated move refresh rate
window.REFRESH_GAME_SCORE = 15*60; // refresh game score every 15 minutes
window.MAX_IDLE_TIME = 4*60; // stop updating map after 4min idling
window.PRECACHE_PLAYER_NAMES_ZOOM = 17; // zoom level to start pre-resolving player names
window.HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20;
window.SIDEBAR_WIDTH = 300;
// chat messages are requested for the visible viewport. On high zoom
// levels this gets pretty pointless, so request messages in at least a
// X km radius.
window.CHAT_MIN_RANGE = 6;
// this controls how far data is being drawn outside the viewport. Set
// it 0 to only draw entities that intersect the current view. A value
// of one will render an area twice the size of the viewport (or some-
// thing like that, Leaflet doc isn’t too specific). Setting it too low
// makes the missing data on move/zoom out more obvious. Setting it too
// high causes too many items to be drawn, making drag&drop sluggish.
window.VIEWPORT_PAD_RATIO = 0.3;

// how many items to request each query
window.CHAT_PUBLIC_ITEMS = 50;
window.CHAT_FACTION_ITEMS = 50;
// how many pixels to the top before requesting new data
window.CHAT_REQUEST_SCROLL_TOP = 200;
window.CHAT_SHRINKED = 60;

// Minimum area to zoom ratio that field MU's will display
window.FIELD_MU_DISPLAY_AREA_ZOOM_RATIO = 0.001;

// Point tolerance for displaying MU's
window.FIELD_MU_DISPLAY_POINT_TOLERANCE = 60

window.COLOR_SELECTED_PORTAL = '#f0f';
window.COLORS = ['#FF9900', '#0088FF', '#03DC03']; // none, res, enl
window.COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4'];
window.COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'};

window.MOD_TYPE = {RES_SHIELD:'Shield', MULTIHACK:'Multi-hack', FORCE_AMP:'Force Amp', HEATSINK:'Heat Sink', TURRET:'Turret', LINK_AMPLIFIER: 'Link Amp'};

// circles around a selected portal that show from where you can hack
// it and how far the portal reaches (i.e. how far links may be made
// from this portal)
window.ACCESS_INDICATOR_COLOR = 'orange';
window.RANGE_INDICATOR_COLOR = 'red'

// by how much pixels should the portal range be expanded on mobile
// devices. This should make clicking them easier.
window.PORTAL_RADIUS_ENLARGE_MOBILE = 5;

window.DEFAULT_PORTAL_IMG = '//commondatastorage.googleapis.com/ingress.com/img/default-portal-image.png';
window.NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit=1&q=';

// INGRESS CONSTANTS /////////////////////////////////////////////////
// http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
window.RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000];
window.MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000];
window.MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000];
window.HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it
window.OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE'];
window.OCTANTS_ARROW = ['→', '↗', '↑', '↖', '←', '↙', '↓', '↘'];
window.DESTROY_RESONATOR = 75; //AP for destroying portal
window.DESTROY_LINK = 187; //AP for destroying link
window.DESTROY_FIELD = 750; //AP for destroying field
window.CAPTURE_PORTAL = 500; //AP for capturing a portal
window.DEPLOY_RESONATOR = 125; //AP for deploying a resonator
window.COMPLETION_BONUS = 250; //AP for deploying all resonators on portal
window.UPGRADE_ANOTHERS_RESONATOR = 65; //AP for upgrading another's resonator
window.MAX_PORTAL_LEVEL = 8;
window.MAX_RESO_PER_PLAYER = [0, 8, 4, 4, 4, 2, 2, 1, 1];

// OTHER MORE-OR-LESS CONSTANTS //////////////////////////////////////
window.TEAM_NONE = 0;
window.TEAM_RES = 1;
window.TEAM_ENL = 2;
window.TEAM_TO_CSS = ['none', 'res', 'enl'];

window.SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2];
window.SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2];
window.EARTH_RADIUS=6378137;
window.DEG2RAD = Math.PI / 180;

// STORAGE ///////////////////////////////////////////////////////////
// global variables used for storage. Most likely READ ONLY. Proper
// way would be to encapsulate them in an anonymous function and write
// getters/setters, but if you are careful enough, this works.
window.refreshTimeout = undefined;
window.urlPortal = null;
window.playersToResolve = [];
window.playersInResolving = [];
window.selectedPortal = null;
window.portalRangeIndicator = null;
window.portalAccessIndicator = null;
window.mapRunsUserAction = false;
//var portalsLayers, linksLayer, fieldsLayer;
var portalsFactionLayers, linksFactionLayers, fieldsFactionLayers;

// contain references to all entities loaded from the server. If render limits are hit,
// not all may be added to the leaflet layers
window.portals = {};
window.links = {};
window.fields = {};

window.resonators = {};

// contain current status(on/off) of overlay layerGroups.
// But you should use isLayerGroupDisplayed(name) to check the status
window.overlayStatus = {};

// plugin framework. Plugins may load earlier than iitc, so don’t
// overwrite data
if(typeof window.plugin !== 'function') window.plugin = function() {};

@@INJECTCODE@@

} // end of wrapper

// inject code into site context
var script = document.createElement('script');
var info = { buildName: '@@BUILDNAME@@', dateTimeVersion: '@@DATETIMEVERSION@@' };
if (this.GM_info && this.GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description };
script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');'));
(document.body || document.head || document.documentElement).appendChild(script);
<|endoftext|>


<commit_before>/*
 * exports toolkit
 * @author  genify(caijf@corp.netease.com)
 * @author  huntbao
 */
'use strict';

let fs = require('fs');
let util = require('util');
let path = require('path');
let _fs = require('./lib/util/file');
let _path = require('./lib/util/path');
let _util = require('./lib/util/util');
let _io = require('./lib/util/io');
let _log = require('./lib/util/logger');
let Builder = require('./lib/nei/builder');
let server = require('./lib/server/server');
let neiDbConst = require('../fb-modules/config/db.json');
let _logger = _log.logger;

class Main {
    /**
     * 构建 nei 工程
     * @param  {object}  arg - 参数类的实例
     * @param  {string}  action - 操作命令
     * @param  {object}  args - 命令行参数对象
     */
    build(arg, action, args) {
        this.args = args;
        this.config = {
            action: action
        };
        let loadedHandler = (ds) => {
            let cwd = process.cwd() + '/';
            this.config.pid = ds.project.id;
            this.ds = ds;
            this.fillArgs();
            // 合并完参数后, 需要重新 format 一下, 并且此时需要取默认值
            this.args = arg.format(this.config.action, this.args, true);
            this.config.outputRoot = _path.normalize(_path.absolute(this.args.output + '/', cwd));
            this.config.neiConfigRoot = `${this.config.outputRoot}nei.${this.config.pid}/`;
            this.checkConfig();
            new Builder({
                config: this.config,
                args: this.args,
                ds: this.ds
            });
        }
        this.loadData(loadedHandler);
    }

    /**
     * 更新 nei 工程规范
     * @param  {object}  arg - 参数类的实例
     * @param  {string}  action - 操作命令
     * @param  {object}  args - 命令行参数对象
     */
    update(arg, action, args) {
        let dir = path.join(process.cwd(), args.output);
        let tryReadConfig = (configFilePath) => {
            if (_fs.exist(configFilePath)) {
                let config = _util.file2json(configFilePath);
                args.key = config.args.key;
                args.specType = config.args.specType;
                this.build(arg, action, args);
            } else {
                _logger.warn(`文件不存在: ${configFilePath}`);
            }
        }
        this.findFile(dir, 'nei.json', (result) => {
            if (result === null) {
                return _logger.warn(`没找到构建工具的配置文件`);
            }
            tryReadConfig(result);
        });
    }

    /**
     * start mock server
     * @param  {object}  args - args object
     */
    server(args) {
        let tryStartServer = (configFilePath) => {
            if (_fs.exist(configFilePath)) {
                let options = {
                    configFilePath: configFilePath,
                    fromNei: true
                };
                server(options);
            } else {
                _logger.warn(`文件不存在: ${configFilePath}`);
            }
        }
        if (args.configFile) {
            let argsFilePath = _path.absolute(
                args.configFile, process.cwd() + '/'
            );
            return tryStartServer(argsFilePath);
        }
        let dir = path.join(process.cwd(), args.output);
        this.findFile(dir, 'server.config.js', (result) => {
            if (result === null) {
                return _logger.warn(`没找到服务配置文件`);
            }
            tryStartServer(result);
        });
    }

    /**
     * 从 NEI 服务器加载项目数据
     * @param {function} callback - 加载成功回调
     */
    loadData(callback) {
        let neiHost = 'http://nei.netease.com/';
        let projectKey = this.args.key;
        let specType = {
            web: neiDbConst.CMN_TYP_WEB,
            aos: neiDbConst.CMN_TYP_AOS,
            ios: neiDbConst.CMN_TYP_IOS,
            test: neiDbConst.CMN_TYP_TEST
        }[this.args.specType] || neiDbConst.CMN_TYP_WEB;
        let url = `${neiHost}/api/projectres/?key=${encodeURIComponent(projectKey)}&spectype=${specType}`;
        url = _path.normalize(url);
        _logger.info('从 NEI 服务器加载数据, 地址: %s', url);
        _io.download(url, (data) => {
            _logger.info(`数据加载完成, 开始解析数据`);
            let json;
            try {
                json = JSON.parse(data);
                if (json.code !== 200) {
                    _logger.error('NEI 服务器异常', json);
                    process.exit(1);
                }
            } catch (ex) {
                _logger.debug('NEI 数据 \n%s', data);
                _logger.error('NEI 数据解析错误: \n%s', ex.stack);
                process.exit(1);
            }
            callback(json.result);
        });
    }

    /**
     * 检测是否存在 nei 配置文件
     */
    checkConfig() {
        let neiConfigFile = `${this.config.neiConfigRoot}/nei.json`;
        let errorMsg = null;
        if (_fs.exist(neiConfigFile)) {
            if (this.config.action === 'build') {
                errorMsg = '项目 %s 已经存在, 请使用 "nei update" 命令更新项目';
            }
        } else {
            if (this.config.action === 'update') {
                errorMsg = '项目 %s 还未构建, 请先使用 "nei build" 命令构建项目';
            }
        }
        if (errorMsg) {
            _logger.error(errorMsg, this.config.pid);
            return process.exit(1);
        }
    }

    /**
     * 填充参数, 合并项目中的命令行参数设置、规范中的命令行参数
     */
    fillArgs() {
        let spec = this.ds.specs[0];
        let specArgsConfig = spec.spec.argsConfig;
        let proArgs = {};
        this.ds.cliargs.forEach(function (cliarg) {
            proArgs[cliarg.key] = cliarg.value;
        });
        let specCliArgDoc = null;
        let findSpecCliArg = (docs) => {
            specCliArgDoc = docs.find((doc) => {
                return doc.id === specArgsConfig;
            });
            if (!specCliArgDoc) {
                docs.forEach((doc) => {
                    findSpecCliArg(doc.children);
                });
            }
        }
        let specArgs = {};
        // 如果规范设置了命令行参数文件
        if (specArgsConfig) {
            findSpecCliArg(spec.docs);
            if (specCliArgDoc) {
                try {
                    specArgs = JSON.parse(specCliArgDoc.content);
                } catch (e) {
                    _logger.error(`规范设置的命令行参数文件, 它的内容不是有效的 json, 请检查是否有多余的逗号或者缺少逗号, 键及字符串类型的值是否都使用了英文双引号等: ${e}`);
                }
            }
        }
        this.args = Object.assign({}, specArgs, proArgs, this.args);
    }

    /**
     * 在指定目录下查找指定文件名的文件
     * @param {string} dir - 指定目录
     * @param {string} fileName - 指定文件名
     * @param {function} callback - 找到文件后的回调
     */
    findFile(dir, fileName, callback) {
        if (_fs.exist(dir)) {
            let list = fs.readdirSync(dir);
            let found = false;
            for (let i = 0, l = list.length; i < l; i++) {
                let p = `${dir}/${list[i]}`;
                if (_fs.isdir(p)) {
                    if (list[i].match(/nei/)) {
                        found = true;
                        callback(`${p}/${fileName}`);
                        break;
                    }
                } else if (list[i] === fileName) {
                    found = true;
                    callback(p);
                    break;
                }
            }
            if (!found) {
                callback(null);
            }
        } else {
            _logger.warn(`项目目录(${dir})不存在`);
        }
    }
}

module.exports = new Main();

<commit_msg>nei3.0: fb-module path bugfix<commit_after>/*
 * exports toolkit
 * @author  genify(caijf@corp.netease.com)
 * @author  huntbao
 */
'use strict';

let fs = require('fs');
let util = require('util');
let path = require('path');
let _fs = require('./lib/util/file');
let _path = require('./lib/util/path');
let _util = require('./lib/util/util');
let _io = require('./lib/util/io');
let _log = require('./lib/util/logger');
let Builder = require('./lib/nei/builder');
let server = require('./lib/server/server');
let neiDbConst = require('./lib/fb-modules/config/db.json');
let _logger = _log.logger;

class Main {
    /**
     * 构建 nei 工程
     * @param  {object}  arg - 参数类的实例
     * @param  {string}  action - 操作命令
     * @param  {object}  args - 命令行参数对象
     */
    build(arg, action, args) {
        this.args = args;
        this.config = {
            action: action
        };
        let loadedHandler = (ds) => {
            let cwd = process.cwd() + '/';
            this.config.pid = ds.project.id;
            this.ds = ds;
            this.fillArgs();
            // 合并完参数后, 需要重新 format 一下, 并且此时需要取默认值
            this.args = arg.format(this.config.action, this.args, true);
            this.config.outputRoot = _path.normalize(_path.absolute(this.args.output + '/', cwd));
            this.config.neiConfigRoot = `${this.config.outputRoot}nei.${this.config.pid}/`;
            this.checkConfig();
            new Builder({
                config: this.config,
                args: this.args,
                ds: this.ds
            });
        }
        this.loadData(loadedHandler);
    }

    /**
     * 更新 nei 工程规范
     * @param  {object}  arg - 参数类的实例
     * @param  {string}  action - 操作命令
     * @param  {object}  args - 命令行参数对象
     */
    update(arg, action, args) {
        let dir = path.join(process.cwd(), args.output);
        let tryReadConfig = (configFilePath) => {
            if (_fs.exist(configFilePath)) {
                let config = _util.file2json(configFilePath);
                args.key = config.args.key;
                args.specType = config.args.specType;
                this.build(arg, action, args);
            } else {
                _logger.warn(`文件不存在: ${configFilePath}`);
            }
        }
        this.findFile(dir, 'nei.json', (result) => {
            if (result === null) {
                return _logger.warn(`没找到构建工具的配置文件`);
            }
            tryReadConfig(result);
        });
    }

    /**
     * start mock server
     * @param  {object}  args - args object
     */
    server(args) {
        let tryStartServer = (configFilePath) => {
            if (_fs.exist(configFilePath)) {
                let options = {
                    configFilePath: configFilePath,
                    fromNei: true
                };
                server(options);
            } else {
                _logger.warn(`文件不存在: ${configFilePath}`);
            }
        }
        if (args.configFile) {
            let argsFilePath = _path.absolute(
                args.configFile, process.cwd() + '/'
            );
            return tryStartServer(argsFilePath);
        }
        let dir = path.join(process.cwd(), args.output);
        this.findFile(dir, 'server.config.js', (result) => {
            if (result === null) {
                return _logger.warn(`没找到服务配置文件`);
            }
            tryStartServer(result);
        });
    }

    /**
     * 从 NEI 服务器加载项目数据
     * @param {function} callback - 加载成功回调
     */
    loadData(callback) {
        let neiHost = 'http://nei.netease.com/';
        let projectKey = this.args.key;
        let specType = {
            web: neiDbConst.CMN_TYP_WEB,
            aos: neiDbConst.CMN_TYP_AOS,
            ios: neiDbConst.CMN_TYP_IOS,
            test: neiDbConst.CMN_TYP_TEST
        }[this.args.specType] || neiDbConst.CMN_TYP_WEB;
        let url = `${neiHost}/api/projectres/?key=${encodeURIComponent(projectKey)}&spectype=${specType}`;
        url = _path.normalize(url);
        _logger.info('从 NEI 服务器加载数据, 地址: %s', url);
        _io.download(url, (data) => {
            _logger.info(`数据加载完成, 开始解析数据`);
            let json;
            try {
                json = JSON.parse(data);
                if (json.code !== 200) {
                    _logger.error('NEI 服务器异常', json);
                    process.exit(1);
                }
            } catch (ex) {
                _logger.debug('NEI 数据 \n%s', data);
                _logger.error('NEI 数据解析错误: \n%s', ex.stack);
                process.exit(1);
            }
            callback(json.result);
        });
    }

    /**
     * 检测是否存在 nei 配置文件
     */
    checkConfig() {
        let neiConfigFile = `${this.config.neiConfigRoot}/nei.json`;
        let errorMsg = null;
        if (_fs.exist(neiConfigFile)) {
            if (this.config.action === 'build') {
                errorMsg = '项目 %s 已经存在, 请使用 "nei update" 命令更新项目';
            }
        } else {
            if (this.config.action === 'update') {
                errorMsg = '项目 %s 还未构建, 请先使用 "nei build" 命令构建项目';
            }
        }
        if (errorMsg) {
            _logger.error(errorMsg, this.config.pid);
            return process.exit(1);
        }
    }

    /**
     * 填充参数, 合并项目中的命令行参数设置、规范中的命令行参数
     */
    fillArgs() {
        let spec = this.ds.specs[0];
        let specArgsConfig = spec.spec.argsConfig;
        let proArgs = {};
        this.ds.cliargs.forEach(function (cliarg) {
            proArgs[cliarg.key] = cliarg.value;
        });
        let specCliArgDoc = null;
        let findSpecCliArg = (docs) => {
            specCliArgDoc = docs.find((doc) => {
                return doc.id === specArgsConfig;
            });
            if (!specCliArgDoc) {
                docs.forEach((doc) => {
                    findSpecCliArg(doc.children);
                });
            }
        }
        let specArgs = {};
        // 如果规范设置了命令行参数文件
        if (specArgsConfig) {
            findSpecCliArg(spec.docs);
            if (specCliArgDoc) {
                try {
                    specArgs = JSON.parse(specCliArgDoc.content);
                } catch (e) {
                    _logger.error(`规范设置的命令行参数文件, 它的内容不是有效的 json, 请检查是否有多余的逗号或者缺少逗号, 键及字符串类型的值是否都使用了英文双引号等: ${e}`);
                }
            }
        }
        this.args = Object.assign({}, specArgs, proArgs, this.args);
    }

    /**
     * 在指定目录下查找指定文件名的文件
     * @param {string} dir - 指定目录
     * @param {string} fileName - 指定文件名
     * @param {function} callback - 找到文件后的回调
     */
    findFile(dir, fileName, callback) {
        if (_fs.exist(dir)) {
            let list = fs.readdirSync(dir);
            let found = false;
            for (let i = 0, l = list.length; i < l; i++) {
                let p = `${dir}/${list[i]}`;
                if (_fs.isdir(p)) {
                    if (list[i].match(/nei/)) {
                        found = true;
                        callback(`${p}/${fileName}`);
                        break;
                    }
                } else if (list[i] === fileName) {
                    found = true;
                    callback(p);
                    break;
                }
            }
            if (!found) {
                callback(null);
            }
        } else {
            _logger.warn(`项目目录(${dir})不存在`);
        }
    }
}

module.exports = new Main();

<|endoftext|>


<commit_before>module.exports.loop = function () {
    for(var name in Game.creeps) {
        var creep = Game.creeps[name];

        if(creep.carry.energy < creep.carryCapacity) {
            var sources = creep.room.find(FIND_SOURCES);
            if(creep.harvest(sources[0]) == ERR_NOT_IN_RANGE) {
                creep.moveTo(sources[0]);
            }
        }
        else {
            if(creep.transfer(Game.spawns['Spawn1'], RESOURCE_ENERGY) == ERR_NOT_IN_RANGE) {
                creep.moveTo(Game.spawns['Spawn1']);
            }
        }
    }

    var harvesterCreepProt = [WORK,CARRY,MOVE];
    // console.log(Game.creeps.length === undefined);
    var numCreeps = Object.keys(Game.creeps).length;
    if(numCreeps < 3 && Game.spawns['Spawn1'].canCreateCreep(harvesterCreepProt)===0){
        Game.spawns['Spawn1'].createCreep(harvesterCreepProt);
    }
}<commit_msg>Repoked basic target resource.<commit_after>module.exports.loop = function () {
    for(var name in Game.creeps) {
        var creep = Game.creeps[name];

        if(creep.carry.energy < creep.carryCapacity) {
            var sources = creep.room.find(FIND_SOURCES);
            if(creep.harvest(sources[1]) == ERR_NOT_IN_RANGE) {
                creep.moveTo(sources[1]);
            }
        }
        else {
            if(creep.transfer(Game.spawns['Spawn1'], RESOURCE_ENERGY) == ERR_NOT_IN_RANGE) {
                creep.moveTo(Game.spawns['Spawn1']);
            }
        }
    }

    var harvesterCreepProt = [WORK,CARRY,MOVE];
    // console.log(Game.creeps.length === undefined);
    var numCreeps = Object.keys(Game.creeps).length;
    if(numCreeps < 3 && Game.spawns['Spawn1'].canCreateCreep(harvesterCreepProt)===0){
        Game.spawns['Spawn1'].createCreep(harvesterCreepProt);
    }
}<|endoftext|>


<commit_before>// directions:
// (anticlockwise)
// ["E", "N", "W", "S"]
//
// polazrization:
// ["H", "V"]

// cordinates
//  >
// V

var SIZE_X = 8;
var SIZE_Y = 8;
var TILE_SIZE = 40;
var TIME_STEP = 1000;
var board = [];
var state = {}; // or some hashtable like-tking
// as of now
// {"9,12,2": {x: 9, y: 12, dir: 2, amp: 1.}}
// as of now, no polarizarion and only real amp
var history = [];

var i, j, d, p, k, v, x, y, tile, v0, v1, h;

main();

function main () {

  var svg = d3.select("body").append("svg")
    .attr("id", "game")
    .attr("height", 600)
    .attr("width", 900);

  for (i = 0; i < SIZE_X; i++) {
    board[i] = [];
    for (j = 0; j < SIZE_Y; j++)
      board[i][j] = "empty";
  }

  board[4][4] = "super_mirror";

  v = {x: 1, y: 4, dir: 0, amp: 0.7};
  state[[v.x, v.y, v.dir]] = v;

  v = {x: 4, y: 1, dir: 3, amp: 0.5};
  state[[v.x, v.y, v.dir]] = v;

  for (i = 0; i < 8; i++) {

    console.log(state);
    history[i] = [];
    for (k in state) {
      v = state[k];
      history[i].push({x: v.x, y: v.y, dir: v.dir, amp: v.amp});  // to copy
    }

    state = propagate(state, board);

  }

  visualize();

}

function visualize () {

  var boardFlat = [];

  for (i = 0; i < SIZE_X; i++) {
    for (j = 0; j < SIZE_Y; j++)
      boardFlat.push({x: i, y: j, val: board[i][j]});
  }

  d3.select("svg").append("g")
    .attr("id", "board")
    .selectAll(".tile")
    .data(boardFlat)
    .enter()
      .append("rect")
        .attr("class", "tile")
        .attr("width", 0.95 * TILE_SIZE)
        .attr("height", 0.95 * TILE_SIZE)
        .attr("x", function (d) { return TILE_SIZE/2 + TILE_SIZE * d.x; })
        .attr("y", function (d) { return TILE_SIZE/2 + TILE_SIZE * d.y; })
        .style("fill", function (d) { return d.val == "empty" ? null : "black"});

  vizStep(0);

}

function vizStep (i) {

  var photons = d3.select("#board").selectAll(".photon");

  photons.remove();

  photons = d3.select("#board").selectAll(".photon")
    .data(history[i]);

  photons.enter()
    .append("circle")
      .attr("class", "photon")
      .attr("r", 10)
      .attr("cx", function (d) { return TILE_SIZE + TILE_SIZE * (d.x - dir2vx(d.dir)); })  // as d.x and d.y are for destination location
      .attr("cy", function (d) { return TILE_SIZE + TILE_SIZE * (d.y - dir2vy(d.dir)); })
      .style("opacity", function (d) { return d.amp; });

  photons.transition()
    .ease([0,1])
    .duration(TIME_STEP)
      .attr("cx", function (d) { return TILE_SIZE + TILE_SIZE * d.x; })
      .attr("cy", function (d) { return TILE_SIZE + TILE_SIZE * d.y; });

  if ( i + 1 < history.length ) {
    setTimeout(vizStep, TIME_STEP, i + 1);
  }

}

function propagate (state0, board) {

  var state1 = {};

  for (k in state0) {
    v0 = state0[k];

    // also actions for photons otu of board

    tile = board[v0.x][v0.y];

    // v1 - warning: in most cases there are plenty outputs

    switch ( tile ) {
      case "empty":
        v1 = {x: v0.x, y: v0.y, dir: v0.dir, amp: v0.amp}; // needed?
        v1.x = v0.x + dir2vx(v0.dir);
        v1.y = v0.y + dir2vy(v0.dir);
        break;
      case "super_mirror":  // simplest
        v1 = {x: v0.x, y: v0.y, dir: v0.dir, amp: v0.amp};
        v1.x = v0.x - dir2vx(v0.dir);
        v1.y = v0.y - dir2vy(v0.dir);
        v1.dir = (v0.dir + 2) % 4;
        break;
    }

    h = [v1.x, v1.y, v1.dir].join(",");  // we can do numerical, if needed for a speedup

    if (h in state1) {
      state1[h].amp += v1.amp;
      if (state1[h].amp === 0) {
        delete state1[h];
      }
    } else {
      state1[h] = v1;
    }

  }

  return state1;

}

function dir2vx (direction) {
  return ((direction + 1) % 2) * (1 - direction);
}

function dir2vy (direction) {
  return (direction % 2) * (direction - 2);
}<commit_msg>history was not working for some crazy reason<commit_after>// directions:
// (anticlockwise)
// ["E", "N", "W", "S"]
//
// polazrization:
// ["H", "V"]

// cordinates
//  >
// V

var SIZE_X = 8;
var SIZE_Y = 8;
var TILE_SIZE = 40;
var TIME_STEP = 1000;
var board = [];
var state = {}; // or some hashtable like-tking
// as of now
// {"9,12,2": {x: 9, y: 12, dir: 2, amp: 1.}}
// as of now, no polarizarion and only real amp
var history2 = [];

var i, j, d, p, k, v, x, y, tile, v0, v1, h;

var main = function () {

  var svg = d3.select("body").append("svg")
    .attr("id", "game")
    .attr("height", 600)
    .attr("width", 900);

  for (i = 0; i < SIZE_X; i++) {
    board[i] = [];
    for (j = 0; j < SIZE_Y; j++)
      board[i][j] = "empty";
  }

  board[4][4] = "super_mirror";

  v = {x: 1, y: 4, dir: 0, amp: 0.7};
  state[[v.x, v.y, v.dir]] = v;

  v = {x: 4, y: 1, dir: 3, amp: 0.5};
  state[[v.x, v.y, v.dir]] = v;

  history2 = [];
  for (i = 0; i < 8; i++) {

    console.log(state);
    history2[i] = [];
    for (k in state) {
      v = state[k];
      history2[i].push({x: v.x, y: v.y, dir: v.dir, amp: v.amp});  // to copy
    }

    state = propagate(state, board);

  }

  visualize();

}

function visualize () {

  var boardFlat = [];

  for (i = 0; i < SIZE_X; i++) {
    for (j = 0; j < SIZE_Y; j++)
      boardFlat.push({x: i, y: j, val: board[i][j]});
  }

  d3.select("svg").append("g")
    .attr("id", "board")
    .selectAll(".tile")
    .data(boardFlat)
    .enter()
      .append("rect")
        .attr("class", "tile")
        .attr("width", 0.95 * TILE_SIZE)
        .attr("height", 0.95 * TILE_SIZE)
        .attr("x", function (d) { return TILE_SIZE/2 + TILE_SIZE * d.x; })
        .attr("y", function (d) { return TILE_SIZE/2 + TILE_SIZE * d.y; })
        .style("fill", function (d) { return d.val == "empty" ? null : "black"});

  vizStep(0);

}

function vizStep (i) {

  var photons = d3.select("#board").selectAll(".photon");

  photons.remove();

  photons = d3.select("#board").selectAll(".photon")
    .data(history2[i]);

  photons.enter()
    .append("circle")
      .attr("class", "photon")
      .attr("r", 10)
      .attr("cx", function (d) { return TILE_SIZE + TILE_SIZE * (d.x - dir2vx(d.dir)); })  // as d.x and d.y are for destination location
      .attr("cy", function (d) { return TILE_SIZE + TILE_SIZE * (d.y - dir2vy(d.dir)); })
      .style("opacity", function (d) { return d.amp; });

  photons.transition()
    .ease([0,1])
    .duration(TIME_STEP)
      .attr("cx", function (d) { return TILE_SIZE + TILE_SIZE * d.x; })
      .attr("cy", function (d) { return TILE_SIZE + TILE_SIZE * d.y; });

  if ( i + 1 < history2.length ) {
    setTimeout(vizStep, TIME_STEP, i + 1);
  }

}

function propagate (state0, board) {

  var state1 = {};

  for (k in state0) {
    v0 = state0[k];

    // also actions for photons otu of board

    tile = board[v0.x][v0.y];

    // v1 - warning: in most cases there are plenty outputs

    switch ( tile ) {
      case "empty":
        v1 = {x: v0.x, y: v0.y, dir: v0.dir, amp: v0.amp}; // needed?
        v1.x = v0.x + dir2vx(v0.dir);
        v1.y = v0.y + dir2vy(v0.dir);
        break;
      case "super_mirror":  // simplest
        v1 = {x: v0.x, y: v0.y, dir: v0.dir, amp: v0.amp};
        v1.x = v0.x - dir2vx(v0.dir);
        v1.y = v0.y - dir2vy(v0.dir);
        v1.dir = (v0.dir + 2) % 4;
        break;
    }

    h = [v1.x, v1.y, v1.dir].join(",");  // we can do numerical, if needed for a speedup

    if (h in state1) {
      state1[h].amp += v1.amp;
      if (state1[h].amp === 0) {
        delete state1[h];
      }
    } else {
      state1[h] = v1;
    }

  }

  return state1;

}

function dir2vx (direction) {
  return ((direction + 1) % 2) * (1 - direction);
}

function dir2vy (direction) {
  return (direction % 2) * (direction - 2);
}

main();<|endoftext|>