<commit_before>/*global define*/
define([
    '../ThirdParty/when',
    './Cartographic',
    './defined',
    './sampleTerrain',
    './DeveloperError',
    './Rectangle'
], function(
    when,
    Cartographic,
    defined,
    sampleTerrain,
    DeveloperError,
    Rectangle) {
    "use strict";

    /**
     * Reports the availability of tiles in a {@link TilingScheme}.
     *
     * @alias TileAvailability
     * @constructor
     *
     * @param {TilingScheme} tilingScheme The tiling scheme in which to report availability.
     * @param {Number} maximumLevel The maximum tile level that is potentially available.
     */
    function TileAvailability(tilingScheme, maximumLevel) {
        this._tilingScheme = tilingScheme;
        this._maximumLevel = maximumLevel;

        this._rootNodes = [];
        for (var y = 0; y < tilingScheme.getNumberOfYTilesAtLevel(); ++y) {
            for (var x = 0; x < tilingScheme.getNumberOfXTilesAtLevel(); ++x) {
                this._rootNodes.push(new QuadtreeNode(tilingScheme, undefined, 0, x, y));
            }
        }
    }

    var rectangleScratch = new Rectangle();

    /**
     * Marks a rectangular range of tiles in a particular level as being available.
     *
     * @param {Number} level The level.
     * @param {Number} startX The X coordinate of the first available tiles at the level.
     * @param {Number} startY The Y coordinate of the first available tiles at the level.
     * @param {Number} endX The X coordinate of the last available tiles at the level.
     * @param {Number} endY The Y coordinate of the last available tiles at the level.
     */
    TileAvailability.prototype.addAvailableTileRange = function(level, startX, startY, endX, endY) {
        var tilingScheme = this._tilingScheme;

        tilingScheme.tileXYToRectangle(startX, startY, level, rectangleScratch);
        var west = rectangleScratch.west;
        var north = rectangleScratch.north;

        tilingScheme.tileXYToRectangle(endX, endY, level, rectangleScratch);
        var east = rectangleScratch.east;
        var south = rectangleScratch.south;

        var rectangleWithLevel = new RectangleWithLevel(level, west, south, east, north);

        for (var i = 0; i < this._rootNodes.length; ++i) {
            var rootNode = this._rootNodes[i];
            if (rectanglesOverlap(rootNode.extent, rectangleWithLevel)) {
                putRectangleInQuadtree(this._maximumLevel, rootNode, rectangleWithLevel);
            }
        }
    };

    /**
     * Determines the level of the most detailed tile covering the position.  The worst-case performance
     * of this function is linear in the number of tile ranges added with {@link TileAvailability#addAvailableTileRange}.
     * It uses a quadtree for lookup so in most cases it will complete in logarithmic time.
     *
     * @param {Cartographic} position The position for which to determine the maximum available level.  The height component is ignored.
     * @return {Number} The level of the most detailed tile covering the position.
     * @throws {DeveloperError} If position is outside any tile according to the tiling scheme.
     */
    TileAvailability.prototype.computeMaximumLevelAtPosition = function(position) {
        // Find the root node that contains this position.
        var node;
        for (var nodeIndex = 0; nodeIndex < this._rootNodes.length; ++nodeIndex) {
            var rootNode = this._rootNodes[nodeIndex];
            if (rectangleContainsPosition(rootNode.extent, position)) {
                node = rootNode;
                break;
            }
        }

        //>>includeStart('debug', pragmas.debug);
        if (!defined(node)) {
            throw new DeveloperError('The specified position does not exist in any root node of the tiling scheme.');
        }
        //>>includeEnd('debug');

        return findMaxLevelFromNode(undefined, node, position);
    };

    function findMaxLevelFromNode(stopNode, node, position) {
        var maxLevel = 0;

        // Find the deepest quadtree node containing this point.
        while (true) {
            var nw = node._nw && rectangleContainsPosition(node._nw.extent, position);
            var ne = node._nw && rectangleContainsPosition(node._ne.extent, position);
            var sw = node._nw && rectangleContainsPosition(node._sw.extent, position);
            var se = node._nw && rectangleContainsPosition(node._se.extent, position);

            // The common scenario is that the point is in only one quadrant and we can simply
            // iterate down the tree.  But if the point is on a boundary between tiles, it is
            // in multiple tiles and we need to check all of them, so use recursion.
            if (nw + ne + sw + se > 1) {
                if (nw) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._nw, position));
                }
                if (ne) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._ne, position));
                }
                if (sw) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._sw, position));
                }
                if (se) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._se, position));
                }
                break;
            } else if (nw) {
                node = node._nw;
            } else if (ne) {
                node = node._ne;
            } else if (sw) {
                node = node._sw;
            } else if (se) {
                node = node._se;
            } else {
                break;
            }
        }

        // Work up the tree until we find a rectangle that contains this point.
        while (node !== stopNode) {
            var rectangles = node.rectangles;

            // Rectangles are sorted by level, lowest first.
            for (var i = rectangles.length - 1; i >= 0 && rectangles[i].level > maxLevel; --i) {
                var rectangle = rectangles[i];
                if (rectangleContainsPosition(rectangle, position)) {
                    maxLevel = rectangle.level;
                }
            }

            node = node.parent;
        }

        return maxLevel;
    }

    var rectanglesScratch = [];
    var remainingToCoverByLevelScratch = [];
    var westScratch = new Rectangle();
    var eastScratch = new Rectangle();

    /**
     * Finds the most detailed level that is available _everywhere_ within a given rectangle.  More detailed
     * tiles may be available in parts of the rectangle, but not the whole thing.  The return value of this
     * function may be safely passed to {@link sampleTerrain} for any position within the rectangle.
     *
     * @param {Rectangle} rectangle The rectangle.
     * @return {Number} The best available level for the entire rectangle.
     */
    TileAvailability.prototype.computeBestAvailableLevelOverRectangle = function(rectangle) {
        var rectangles = rectanglesScratch;
        rectangles.length = 0;

        if (rectangle.east < rectangle.west) {
            // Rectangle crosses the IDL, make it two rectangles.
            rectangles.push(Rectangle.fromRadians(-Math.PI, rectangle.south, rectangle.east, rectangle.north, westScratch));
            rectangles.push(Rectangle.fromRadians(rectangle.west, rectangle.south, Math.PI, rectangle.north, eastScratch));
        } else {
            rectangles.push(rectangle);
        }

        var remainingToCoverByLevel = remainingToCoverByLevelScratch;
        remainingToCoverByLevel.length = 0;

        var i;
        for (i = 0; i < this._rootNodes.length; ++i) {
            updateCoverageWithNode(remainingToCoverByLevel, this._rootNodes[i], rectangles);
        }

        for (i = remainingToCoverByLevel.length - 1; i >= 0; --i) {
            if (remainingToCoverByLevel[i] && remainingToCoverByLevel[i].length === 0) {
                return i;
            }
        }

        return 0;
    };

    var cartographicScratch = new Cartographic();

    /**
     * Determines if a particular tile is available.
     * @param {Number} level The tile level to check.
     * @param {Number} x The X coordinate of the tile to check.
     * @param {Number} y The Y coordinate of the tile to check.
     * @return {Boolean} True if the tile is available; otherwise, false.
     */
    TileAvailability.prototype.isTileAvailable = function(level, x, y) {
        // Get the center of the tile and find the maximum level at that position.
        // Because availability is by tile, if the level is available at that point, it
        // is sure to be available for the whole tile.  We assume that if a tile at level n exists,
        // then all its parent tiles back to level 0 exist too.  This isn't really enforced
        // anywhere, but Cesium would never load a tile for which this is not true.
        var rectangle = this._tilingScheme.tileXYToRectangle(x, y, level, rectangleScratch);
        Rectangle.center(rectangle, cartographicScratch);
        return this.computeMaximumLevelAtPosition(cartographicScratch) >= level;
    };

    /**
     * Computes a bit mask indicating which of a tile's four children exist.
     * If a child's bit is set, a tile is available for that child.  If it is cleared,
     * the tile is not available.  The bit values are as follows:
     *

     *
Bit Position	Bit Value	Child Tile

     *
0	1	Southwest

     *
1	2	Southeast

     *
2	4	Northwest

     *
3	8	Northeast

     *

     *
     * @param {Number} level The level of the parent tile.
     * @param {Number} x The X coordinate of the parent tile.
     * @param {Number} y The Y coordinate of the parent tile.
     * @return {Number} The bit mask indicating child availability.
     */
    TileAvailability.prototype.computeChildMaskForTile = function(level, x, y) {
        var childLevel = level + 1;
        if (childLevel >= this._maximumLevel) {
            return 0;
        }

        var mask = 0;

        mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y + 1) ? 1 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y + 1) ? 2 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y) ? 4 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y) ? 8 : 0;

        return mask;
    };

    function QuadtreeNode(tilingScheme, parent, level, x, y) {
        this.tilingScheme = tilingScheme;
        this.parent = parent;
        this.level = level;
        this.x = x;
        this.y = y;
        this.extent = tilingScheme.tileXYToRectangle(x, y, level);

        this.rectangles = [];
        this._sw = undefined;
        this._se = undefined;
        this._nw = undefined;
        this._ne = undefined;
    }

    Object.defineProperties(QuadtreeNode.prototype, {
        nw: {
            get: function() {
                if (!this._nw) {
                    this._nw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2);
                }
                return this._nw;
            }
        },

        ne: {
            get: function() {
                if (!this._ne) {
                    this._ne = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2);
                }
                return this._ne;
            }
        },

        sw: {
            get: function() {
                if (!this._sw) {
                    this._sw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2 + 1);
                }
                return this._sw;
            }
        },

        se: {
            get: function() {
                if (!this._se) {
                    this._se = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2 + 1);
                }
                return this._se;
            }
        }
    });

    function RectangleWithLevel(level, west, south, east, north) {
        this.level = level;
        this.west = west;
        this.south = south;
        this.east = east;
        this.north = north;
    }

    function rectanglesOverlap(rectangle1, rectangle2) {
        var west = Math.max(rectangle1.west, rectangle2.west);
        var south = Math.max(rectangle1.south, rectangle2.south);
        var east = Math.min(rectangle1.east, rectangle2.east);
        var north = Math.min(rectangle1.north, rectangle2.north);
        return south < north && west < east;
    }

    function putRectangleInQuadtree(maxDepth, node, rectangle) {
        while (node.level < maxDepth) {
            if (rectangleFullyContainsRectangle(node.nw.extent, rectangle)) {
                node = node.nw;
            } else if (rectangleFullyContainsRectangle(node.ne.extent, rectangle)) {
                node = node.ne;
            } else if (rectangleFullyContainsRectangle(node.sw.extent, rectangle)) {
                node = node.sw;
            } else if (rectangleFullyContainsRectangle(node.se.extent, rectangle)) {
                node = node.se;
            } else {
                break;
            }
        }

        node.rectangles.push(rectangle);
    }

    function rectangleFullyContainsRectangle(potentialContainer, rectangleToTest) {
        return rectangleToTest.west >= potentialContainer.west &&
               rectangleToTest.east <= potentialContainer.east &&
               rectangleToTest.south >= potentialContainer.south &&
               rectangleToTest.north <= potentialContainer.north;
    }

    function rectangleContainsPosition(potentialContainer, positionToTest) {
        return positionToTest.longitude >= potentialContainer.west &&
               positionToTest.longitude <= potentialContainer.east &&
               positionToTest.latitude >= potentialContainer.south &&
               positionToTest.latitude <= potentialContainer.north;
    }

    function updateCoverageWithNode(remainingToCoverByLevel, node, rectanglesToCover) {
        if (!node) {
            return;
        }

        var i;
        var anyOverlap = false;
        for (i = 0; i < rectanglesToCover.length; ++i) {
            anyOverlap = anyOverlap || rectanglesOverlap(node.extent, rectanglesToCover[i]);
        }

        if (!anyOverlap) {
            // This node is not applicable to the rectangle(s).
            return;
        }

        var rectangles = node.rectangles;
        for (i = 0; i < rectangles.length; ++i) {
            var rectangle = rectangles[i];

            if (!remainingToCoverByLevel[rectangle.level]) {
                remainingToCoverByLevel[rectangle.level] = rectanglesToCover;
            }

            remainingToCoverByLevel[rectangle.level] = subtractRectangle(remainingToCoverByLevel[rectangle.level], rectangle);
        }

        // Update with child nodes.
        updateCoverageWithNode(remainingToCoverByLevel, node._nw, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._ne, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._sw, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._se, rectanglesToCover);
    }

    function subtractRectangle(rectangleList, rectangleToSubtract) {
        var result = [];
        for (var i = 0; i < rectangleList.length; ++i) {
            var rectangle = rectangleList[i];
            if (!rectanglesOverlap(rectangle, rectangleToSubtract)) {
                // Disjoint rectangles.  Original rectangle is unmodified.
                result.push(rectangle);
            } else {
                // rectangleToSubtract partially or completely overlaps rectangle.
                if (rectangle.west < rectangleToSubtract.west) {
                    result.push(new Rectangle(rectangle.west, rectangle.south, rectangleToSubtract.west, rectangle.north));
                }
                if (rectangle.east > rectangleToSubtract.east) {
                    result.push(new Rectangle(rectangleToSubtract.east, rectangle.south, rectangle.east, rectangle.north));
                }
                if (rectangle.south < rectangleToSubtract.south) {
                    result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangle.south, Math.min(rectangleToSubtract.east, rectangle.east), rectangleToSubtract.south));
                }
                if (rectangle.north > rectangleToSubtract.north) {
                    result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangleToSubtract.north, Math.min(rectangleToSubtract.east, rectangle.east), rectangle.north));
                }
            }
        }

        return result;
    }

    return TileAvailability;
});
<commit_msg>Functions at the end.<commit_after>/*global define*/
define([
    '../ThirdParty/when',
    './Cartographic',
    './defined',
    './sampleTerrain',
    './DeveloperError',
    './Rectangle'
], function(
    when,
    Cartographic,
    defined,
    sampleTerrain,
    DeveloperError,
    Rectangle) {
    "use strict";

    /**
     * Reports the availability of tiles in a {@link TilingScheme}.
     *
     * @alias TileAvailability
     * @constructor
     *
     * @param {TilingScheme} tilingScheme The tiling scheme in which to report availability.
     * @param {Number} maximumLevel The maximum tile level that is potentially available.
     */
    function TileAvailability(tilingScheme, maximumLevel) {
        this._tilingScheme = tilingScheme;
        this._maximumLevel = maximumLevel;

        this._rootNodes = [];
        for (var y = 0; y < tilingScheme.getNumberOfYTilesAtLevel(); ++y) {
            for (var x = 0; x < tilingScheme.getNumberOfXTilesAtLevel(); ++x) {
                this._rootNodes.push(new QuadtreeNode(tilingScheme, undefined, 0, x, y));
            }
        }
    }

    var rectangleScratch = new Rectangle();

    /**
     * Marks a rectangular range of tiles in a particular level as being available.
     *
     * @param {Number} level The level.
     * @param {Number} startX The X coordinate of the first available tiles at the level.
     * @param {Number} startY The Y coordinate of the first available tiles at the level.
     * @param {Number} endX The X coordinate of the last available tiles at the level.
     * @param {Number} endY The Y coordinate of the last available tiles at the level.
     */
    TileAvailability.prototype.addAvailableTileRange = function(level, startX, startY, endX, endY) {
        var tilingScheme = this._tilingScheme;

        tilingScheme.tileXYToRectangle(startX, startY, level, rectangleScratch);
        var west = rectangleScratch.west;
        var north = rectangleScratch.north;

        tilingScheme.tileXYToRectangle(endX, endY, level, rectangleScratch);
        var east = rectangleScratch.east;
        var south = rectangleScratch.south;

        var rectangleWithLevel = new RectangleWithLevel(level, west, south, east, north);

        for (var i = 0; i < this._rootNodes.length; ++i) {
            var rootNode = this._rootNodes[i];
            if (rectanglesOverlap(rootNode.extent, rectangleWithLevel)) {
                putRectangleInQuadtree(this._maximumLevel, rootNode, rectangleWithLevel);
            }
        }
    };

    /**
     * Determines the level of the most detailed tile covering the position.  The worst-case performance
     * of this function is linear in the number of tile ranges added with {@link TileAvailability#addAvailableTileRange}.
     * It uses a quadtree for lookup so in most cases it will complete in logarithmic time.
     *
     * @param {Cartographic} position The position for which to determine the maximum available level.  The height component is ignored.
     * @return {Number} The level of the most detailed tile covering the position.
     * @throws {DeveloperError} If position is outside any tile according to the tiling scheme.
     */
    TileAvailability.prototype.computeMaximumLevelAtPosition = function(position) {
        // Find the root node that contains this position.
        var node;
        for (var nodeIndex = 0; nodeIndex < this._rootNodes.length; ++nodeIndex) {
            var rootNode = this._rootNodes[nodeIndex];
            if (rectangleContainsPosition(rootNode.extent, position)) {
                node = rootNode;
                break;
            }
        }

        //>>includeStart('debug', pragmas.debug);
        if (!defined(node)) {
            throw new DeveloperError('The specified position does not exist in any root node of the tiling scheme.');
        }
        //>>includeEnd('debug');

        return findMaxLevelFromNode(undefined, node, position);
    };

    var rectanglesScratch = [];
    var remainingToCoverByLevelScratch = [];
    var westScratch = new Rectangle();
    var eastScratch = new Rectangle();

    /**
     * Finds the most detailed level that is available _everywhere_ within a given rectangle.  More detailed
     * tiles may be available in parts of the rectangle, but not the whole thing.  The return value of this
     * function may be safely passed to {@link sampleTerrain} for any position within the rectangle.
     *
     * @param {Rectangle} rectangle The rectangle.
     * @return {Number} The best available level for the entire rectangle.
     */
    TileAvailability.prototype.computeBestAvailableLevelOverRectangle = function(rectangle) {
        var rectangles = rectanglesScratch;
        rectangles.length = 0;

        if (rectangle.east < rectangle.west) {
            // Rectangle crosses the IDL, make it two rectangles.
            rectangles.push(Rectangle.fromRadians(-Math.PI, rectangle.south, rectangle.east, rectangle.north, westScratch));
            rectangles.push(Rectangle.fromRadians(rectangle.west, rectangle.south, Math.PI, rectangle.north, eastScratch));
        } else {
            rectangles.push(rectangle);
        }

        var remainingToCoverByLevel = remainingToCoverByLevelScratch;
        remainingToCoverByLevel.length = 0;

        var i;
        for (i = 0; i < this._rootNodes.length; ++i) {
            updateCoverageWithNode(remainingToCoverByLevel, this._rootNodes[i], rectangles);
        }

        for (i = remainingToCoverByLevel.length - 1; i >= 0; --i) {
            if (remainingToCoverByLevel[i] && remainingToCoverByLevel[i].length === 0) {
                return i;
            }
        }

        return 0;
    };

    var cartographicScratch = new Cartographic();

    /**
     * Determines if a particular tile is available.
     * @param {Number} level The tile level to check.
     * @param {Number} x The X coordinate of the tile to check.
     * @param {Number} y The Y coordinate of the tile to check.
     * @return {Boolean} True if the tile is available; otherwise, false.
     */
    TileAvailability.prototype.isTileAvailable = function(level, x, y) {
        // Get the center of the tile and find the maximum level at that position.
        // Because availability is by tile, if the level is available at that point, it
        // is sure to be available for the whole tile.  We assume that if a tile at level n exists,
        // then all its parent tiles back to level 0 exist too.  This isn't really enforced
        // anywhere, but Cesium would never load a tile for which this is not true.
        var rectangle = this._tilingScheme.tileXYToRectangle(x, y, level, rectangleScratch);
        Rectangle.center(rectangle, cartographicScratch);
        return this.computeMaximumLevelAtPosition(cartographicScratch) >= level;
    };

    /**
     * Computes a bit mask indicating which of a tile's four children exist.
     * If a child's bit is set, a tile is available for that child.  If it is cleared,
     * the tile is not available.  The bit values are as follows:
     *

     *
Bit Position	Bit Value	Child Tile

     *
0	1	Southwest

     *
1	2	Southeast

     *
2	4	Northwest

     *
3	8	Northeast

     *

     *
     * @param {Number} level The level of the parent tile.
     * @param {Number} x The X coordinate of the parent tile.
     * @param {Number} y The Y coordinate of the parent tile.
     * @return {Number} The bit mask indicating child availability.
     */
    TileAvailability.prototype.computeChildMaskForTile = function(level, x, y) {
        var childLevel = level + 1;
        if (childLevel >= this._maximumLevel) {
            return 0;
        }

        var mask = 0;

        mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y + 1) ? 1 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y + 1) ? 2 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y) ? 4 : 0;
        mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y) ? 8 : 0;

        return mask;
    };

    function QuadtreeNode(tilingScheme, parent, level, x, y) {
        this.tilingScheme = tilingScheme;
        this.parent = parent;
        this.level = level;
        this.x = x;
        this.y = y;
        this.extent = tilingScheme.tileXYToRectangle(x, y, level);

        this.rectangles = [];
        this._sw = undefined;
        this._se = undefined;
        this._nw = undefined;
        this._ne = undefined;
    }

    Object.defineProperties(QuadtreeNode.prototype, {
        nw: {
            get: function() {
                if (!this._nw) {
                    this._nw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2);
                }
                return this._nw;
            }
        },

        ne: {
            get: function() {
                if (!this._ne) {
                    this._ne = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2);
                }
                return this._ne;
            }
        },

        sw: {
            get: function() {
                if (!this._sw) {
                    this._sw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2 + 1);
                }
                return this._sw;
            }
        },

        se: {
            get: function() {
                if (!this._se) {
                    this._se = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2 + 1);
                }
                return this._se;
            }
        }
    });

    function RectangleWithLevel(level, west, south, east, north) {
        this.level = level;
        this.west = west;
        this.south = south;
        this.east = east;
        this.north = north;
    }

    function rectanglesOverlap(rectangle1, rectangle2) {
        var west = Math.max(rectangle1.west, rectangle2.west);
        var south = Math.max(rectangle1.south, rectangle2.south);
        var east = Math.min(rectangle1.east, rectangle2.east);
        var north = Math.min(rectangle1.north, rectangle2.north);
        return south < north && west < east;
    }

    function putRectangleInQuadtree(maxDepth, node, rectangle) {
        while (node.level < maxDepth) {
            if (rectangleFullyContainsRectangle(node.nw.extent, rectangle)) {
                node = node.nw;
            } else if (rectangleFullyContainsRectangle(node.ne.extent, rectangle)) {
                node = node.ne;
            } else if (rectangleFullyContainsRectangle(node.sw.extent, rectangle)) {
                node = node.sw;
            } else if (rectangleFullyContainsRectangle(node.se.extent, rectangle)) {
                node = node.se;
            } else {
                break;
            }
        }

        node.rectangles.push(rectangle);
    }

    function rectangleFullyContainsRectangle(potentialContainer, rectangleToTest) {
        return rectangleToTest.west >= potentialContainer.west &&
               rectangleToTest.east <= potentialContainer.east &&
               rectangleToTest.south >= potentialContainer.south &&
               rectangleToTest.north <= potentialContainer.north;
    }

    function rectangleContainsPosition(potentialContainer, positionToTest) {
        return positionToTest.longitude >= potentialContainer.west &&
               positionToTest.longitude <= potentialContainer.east &&
               positionToTest.latitude >= potentialContainer.south &&
               positionToTest.latitude <= potentialContainer.north;
    }

    function findMaxLevelFromNode(stopNode, node, position) {
        var maxLevel = 0;

        // Find the deepest quadtree node containing this point.
        while (true) {
            var nw = node._nw && rectangleContainsPosition(node._nw.extent, position);
            var ne = node._nw && rectangleContainsPosition(node._ne.extent, position);
            var sw = node._nw && rectangleContainsPosition(node._sw.extent, position);
            var se = node._nw && rectangleContainsPosition(node._se.extent, position);

            // The common scenario is that the point is in only one quadrant and we can simply
            // iterate down the tree.  But if the point is on a boundary between tiles, it is
            // in multiple tiles and we need to check all of them, so use recursion.
            if (nw + ne + sw + se > 1) {
                if (nw) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._nw, position));
                }
                if (ne) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._ne, position));
                }
                if (sw) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._sw, position));
                }
                if (se) {
                    maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._se, position));
                }
                break;
            } else if (nw) {
                node = node._nw;
            } else if (ne) {
                node = node._ne;
            } else if (sw) {
                node = node._sw;
            } else if (se) {
                node = node._se;
            } else {
                break;
            }
        }

        // Work up the tree until we find a rectangle that contains this point.
        while (node !== stopNode) {
            var rectangles = node.rectangles;

            // Rectangles are sorted by level, lowest first.
            for (var i = rectangles.length - 1; i >= 0 && rectangles[i].level > maxLevel; --i) {
                var rectangle = rectangles[i];
                if (rectangleContainsPosition(rectangle, position)) {
                    maxLevel = rectangle.level;
                }
            }

            node = node.parent;
        }

        return maxLevel;
    }

    function updateCoverageWithNode(remainingToCoverByLevel, node, rectanglesToCover) {
        if (!node) {
            return;
        }

        var i;
        var anyOverlap = false;
        for (i = 0; i < rectanglesToCover.length; ++i) {
            anyOverlap = anyOverlap || rectanglesOverlap(node.extent, rectanglesToCover[i]);
        }

        if (!anyOverlap) {
            // This node is not applicable to the rectangle(s).
            return;
        }

        var rectangles = node.rectangles;
        for (i = 0; i < rectangles.length; ++i) {
            var rectangle = rectangles[i];

            if (!remainingToCoverByLevel[rectangle.level]) {
                remainingToCoverByLevel[rectangle.level] = rectanglesToCover;
            }

            remainingToCoverByLevel[rectangle.level] = subtractRectangle(remainingToCoverByLevel[rectangle.level], rectangle);
        }

        // Update with child nodes.
        updateCoverageWithNode(remainingToCoverByLevel, node._nw, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._ne, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._sw, rectanglesToCover);
        updateCoverageWithNode(remainingToCoverByLevel, node._se, rectanglesToCover);
    }

    function subtractRectangle(rectangleList, rectangleToSubtract) {
        var result = [];
        for (var i = 0; i < rectangleList.length; ++i) {
            var rectangle = rectangleList[i];
            if (!rectanglesOverlap(rectangle, rectangleToSubtract)) {
                // Disjoint rectangles.  Original rectangle is unmodified.
                result.push(rectangle);
            } else {
                // rectangleToSubtract partially or completely overlaps rectangle.
                if (rectangle.west < rectangleToSubtract.west) {
                    result.push(new Rectangle(rectangle.west, rectangle.south, rectangleToSubtract.west, rectangle.north));
                }
                if (rectangle.east > rectangleToSubtract.east) {
                    result.push(new Rectangle(rectangleToSubtract.east, rectangle.south, rectangle.east, rectangle.north));
                }
                if (rectangle.south < rectangleToSubtract.south) {
                    result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangle.south, Math.min(rectangleToSubtract.east, rectangle.east), rectangleToSubtract.south));
                }
                if (rectangle.north > rectangleToSubtract.north) {
                    result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangleToSubtract.north, Math.min(rectangleToSubtract.east, rectangle.east), rectangle.north));
                }
            }
        }

        return result;
    }

    return TileAvailability;
});
<|endoftext|>


<commit_before>'use strict';

(function (angular, buildfire) {
  angular
    .module('seminarNotesPluginContent')
    .controller('ContentHomeCtrl', ['$scope', 'TAG_NAMES', 'STATUS_CODE', 'DataStore', 'LAYOUTS', '$sce', 'PAGINATION', 'Buildfire', '$modal', '$rootScope',
      function ($scope, TAG_NAMES, STATUS_CODE, DataStore, LAYOUTS, $sce, PAGINATION, Buildfire, $modal, $rootScope) {

        var ContentHome = this;

        var _data = {
          "content": {
            "carouselImages": [],
            "description": ""
          },
          "design": {
            "itemListLayout": LAYOUTS.itemListLayout[0].name,
            "itemListBgImage": ""
          }
        };

        ContentHome.descriptionWYSIWYGOptions = {
          plugins: 'advlist autolink link image lists charmap print preview',
          skin: 'lightgray',
          trusted: true,
          theme: 'modern'
        };

        // create a new instance of the buildfire carousel editor
        var editor = new Buildfire.components.carousel.editor("#carousel");

        // this method will be called when a new item added to the list
        editor.onAddItems = function (items) {
          if (!ContentHome.data.content.carouselImages)
            ContentHome.data.content.carouselImages = [];
          ContentHome.data.content.carouselImages.push.apply(ContentHome.data.content.carouselImages, items);
          $scope.$digest();
        };
        // this method will be called when an item deleted from the list
        editor.onDeleteItem = function (item, index) {
          ContentHome.data.content.carouselImages.splice(index, 1);
          $scope.$digest();
        };
        // this method will be called when you edit item details
        editor.onItemChange = function (item, index) {
          ContentHome.data.content.carouselImages.splice(index, 1, item);
          $scope.$digest();
        };
        // this method will be called when you change the order of items
        editor.onOrderChange = function (item, oldIndex, newIndex) {
          var items = ContentHome.data.content.carouselImages;

          var tmp = items[oldIndex];

          if (oldIndex < newIndex) {
            for (var i = oldIndex + 1; i <= newIndex; i++) {
              items[i - 1] = items[i];
            }
          } else {
            for (var i = oldIndex - 1; i >= newIndex; i--) {
              items[i + 1] = items[i];
            }
          }
          items[newIndex] = tmp;

          ContentHome.data.content.carouselImages = items;
          $scope.$digest();
        };
      }]);
})(window.angular, window.buildfire);
<commit_msg>Added delay to autosave the data<commit_after>'use strict';

(function (angular, buildfire) {
  angular
    .module('seminarNotesPluginContent')
    .controller('ContentHomeCtrl', ['$scope', 'TAG_NAMES', 'STATUS_CODE', 'DataStore', 'LAYOUTS', '$sce', 'PAGINATION', 'Buildfire', '$modal', '$rootScope',
      function ($scope, TAG_NAMES, STATUS_CODE, DataStore, LAYOUTS, $sce, PAGINATION, Buildfire, $modal, $rootScope) {

        var ContentHome = this;

        var _data = {
          "content": {
            "carouselImages": [],
            "description": ""
          },
          "design": {
            "itemListLayout": LAYOUTS.itemListLayout[0].name,
            "itemListBgImage": ""
          }
        };

        /*
         * ContentHome.data used to store EventsInfo which from datastore.
         */
        ContentHome.masterData = null;

        /*
         * create an artificial delay so api isnt called on every character entered
         * */
        var tmrDelay = null;

        var updateMasterItem = function (data) {
          ContentHome.masterData = angular.copy(data);
        };

        var isUnchanged = function (data) {
          return angular.equals(data, ContentHome.masterData);
        };

        /*
         * Go pull any previously saved data
         * */
        var init = function () {
          var success = function (result) {

              console.info('Init success result:', result);
              ContentHome.data = result.data;
              if (!ContentHome.data) {
                ContentHome.data = angular.copy(_data);
              } else {
                if (!ContentHome.data.content)
                  ContentHome.data.content = {};
                if (!ContentHome.data.content.carouselImages)
                  editor.loadItems([]);
                else
                  editor.loadItems(ContentHome.data.content.carouselImages);
              }
              updateMasterItem(ContentHome.data);
              if (tmrDelay)clearTimeout(tmrDelay);
            }
            , error = function (err) {
              if (err && err.code !== STATUS_CODE.NOT_FOUND) {
                console.error('Error while getting data', err);
                if (tmrDelay)clearTimeout(tmrDelay);
              }
              else if (err && err.code === STATUS_CODE.NOT_FOUND) {
                saveData(JSON.parse(angular.toJson(ContentHome.data)), TAG_NAMES.SEMINAR_INFO);
              }
            };
          DataStore.get(TAG_NAMES.SEMINAR_INFO).then(success, error);
        };

        ContentHome.descriptionWYSIWYGOptions = {
          plugins: 'advlist autolink link image lists charmap print preview',
          skin: 'lightgray',
          trusted: true,
          theme: 'modern'
        };

        // create a new instance of the buildfire carousel editor
        var editor = new Buildfire.components.carousel.editor("#carousel");

        // this method will be called when a new item added to the list
        editor.onAddItems = function (items) {
          if (!ContentHome.data.content.carouselImages)
            ContentHome.data.content.carouselImages = [];
          ContentHome.data.content.carouselImages.push.apply(ContentHome.data.content.carouselImages, items);
          $scope.$digest();
        };
        // this method will be called when an item deleted from the list
        editor.onDeleteItem = function (item, index) {
          ContentHome.data.content.carouselImages.splice(index, 1);
          $scope.$digest();
        };
        // this method will be called when you edit item details
        editor.onItemChange = function (item, index) {
          ContentHome.data.content.carouselImages.splice(index, 1, item);
          $scope.$digest();
        };
        // this method will be called when you change the order of items
        editor.onOrderChange = function (item, oldIndex, newIndex) {
          var items = ContentHome.data.content.carouselImages;

          var tmp = items[oldIndex];

          if (oldIndex < newIndex) {
            for (var i = oldIndex + 1; i <= newIndex; i++) {
              items[i - 1] = items[i];
            }
          } else {
            for (var i = oldIndex - 1; i >= newIndex; i--) {
              items[i + 1] = items[i];
            }
          }
          items[newIndex] = tmp;

          ContentHome.data.content.carouselImages = items;
          $scope.$digest();
        };

        /*
         * Call the datastore to save the data object
         */
        var saveData = function (newObj, tag) {
          if (typeof newObj === 'undefined') {
            return;
          }
          var success = function (result) {
              console.info('Saved data result: ', result);
              updateMasterItem(newObj);
            }
            , error = function (err) {
              console.error('Error while saving data : ', err);
            };
          DataStore.save(newObj, tag).then(success, error);
        };

        var saveDataWithDelay = function (newObj) {
          if (newObj) {
            if (isUnchanged(newObj)) {
              return;
            }
            if (tmrDelay) {
              clearTimeout(tmrDelay);
            }
            tmrDelay = setTimeout(function () {
              saveData(JSON.parse(angular.toJson(newObj)), TAG_NAMES.SEMINAR_INFO);
            }, 500);
          }
        };

        init();

        updateMasterItem(_data);

        /*
         * watch for changes in data and trigger the saveDataWithDelay function on change
         * */
        $scope.$watch(function () {
          return ContentHome.data;
        }, saveDataWithDelay, true);
      }]);
})(window.angular, window.buildfire);
<|endoftext|>


<commit_before>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero 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, either version 3 of the License, or
    (at your option) any later version.

    Zotero 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 Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.ProgressWindowSet = new function() {
	this.add = add;
	this.tile = tile;
	this.remove = remove;
	this.updateTimers = updateTimers;

	var _progressWindows = [];

	const X_OFFSET = 25;
	const Y_OFFSET = 35;
	const Y_SEPARATOR = 12;
	const X_WINDOWLESS_OFFSET = 50;
	const Y_WINDOWLESS_OFFSET = 100;

	function add(progressWindow, instance) {
		_progressWindows.push({
			progressWindow: progressWindow,
			instance: instance
		});
	}

	function tile(progressWin) {
		var parent = progressWin.opener;
		var y_sub = null;

		for (var i=0; i<_progressWindows.length; i++) {
			var p = _progressWindows[i].progressWindow;

			// Skip progress windows from other windows
			if (p.opener != parent) {
				continue;
			}

			if (!y_sub) {
				y_sub = Y_OFFSET + p.outerHeight;
			}

			if (parent) {
				var right = parent.screenX + parent.outerWidth;
				var bottom = parent.screenY + parent.outerHeight;
			}
			else {
				var right = progressWin.screen.width + X_OFFSET - X_WINDOWLESS_OFFSET;
				var bottom = progressWin.screen.height + Y_OFFSET - Y_WINDOWLESS_OFFSET;
			}

			p.moveTo(right - p.outerWidth - X_OFFSET, bottom - y_sub);

			y_sub += p.outerHeight + Y_SEPARATOR;
		}
	}

	function remove(progressWin) {
		for (var i=0; i<_progressWindows.length; i++) {
			if (_progressWindows[i].progressWindow == progressWin) {
				_progressWindows.splice(i, 1);
			}
		}
	}

	function updateTimers() {
		if (!_progressWindows.length) {
			return;
		}

		for (var i=0; i<_progressWindows.length; i++) {
			// Pass |requireMouseOver| so that the window only closes
			// if the mouse was over it at some point
			_progressWindows[i].instance.startCloseTimer(null, true);
		}
	}
}

/*
 * Handles the display of a div showing progress in scraping, indexing, etc.
 *
 * Pass the active window into the constructor
 */
Zotero.ProgressWindow = function(_window){
	var self = this,
		_window = null,
		_progressWindow = null,
		_windowLoaded = false,
		_windowLoading = false,
		_timeoutID = false,
		_closing = false,
		_mouseWasOver = false,
		_deferredUntilWindowLoad = [],
		_deferredUntilWindowLoadThis = [],
		_deferredUntilWindowLoadArgs = [];

	/**
	 * Shows the progress window
	 */
	this.show = function show() {
		if(_windowLoading || _windowLoaded) {	// already loading or loaded
			return false;
		}

		var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"].
					getService(Components.interfaces.nsIWindowWatcher);

		if (!_window){
			_window = Components.classes["@mozilla.org/appshell/window-mediator;1"]
				.getService(Components.interfaces.nsIWindowMediator)
				.getMostRecentWindow("navigator:browser");
		}

		if (_window) {
			_progressWindow = _window.openDialog("chrome://zotero/content/progressWindow.xul",
				"", "chrome,dialog=no,titlebar=no,popup=yes");
		}
		else {
			_progressWindow = ww.openWindow(null, "chrome://zotero/content/progressWindow.xul",
				"", "chrome,dialog=no,titlebar=no,popup=yes", null);
		}
		_progressWindow.addEventListener("load", _onWindowLoaded, false);
		_progressWindow.addEventListener("mouseover", _onMouseOver, false);
		_progressWindow.addEventListener("mouseout", _onMouseOut, false);
		_progressWindow.addEventListener("mouseup", _onMouseUp, false);

		_windowLoading = true;

		Zotero.ProgressWindowSet.add(_progressWindow, this);

		return true;
	}

	/**
	 * Changes the "headline" shown at the top of the progress window
	 */
	this.changeHeadline = _deferUntilWindowLoad(function changeHeadline(text, icon, postText) {
		var doc = _progressWindow.document,
			headline = doc.getElementById("zotero-progress-text-headline");
		while(headline.hasChildNodes()) headline.removeChild(headline.firstChild);

		var preNode = doc.createElement("label");
		preNode.setAttribute("value", text);
		preNode.setAttribute("crop", "end");
		headline.appendChild(preNode);

		if(icon) {
			var img = doc.createElement("image");
			img.width = 16;
			img.height = 16;
			img.setAttribute("src", icon);
			headline.appendChild(img);
		}

		if(postText) {
			var postNode = doc.createElement("label");
			postNode.style.marginLeft = 0;
			postNode.setAttribute("value", " "+postText);
			postNode.setAttribute("crop", "end");
			postNode.setAttribute("flex", "1");
			headline.appendChild(postNode);
		}
	});

	/**
	 * Adds a line to the progress window with the specified icon
	 */
	this.addLines = _deferUntilWindowLoad(function addLines(labels, icons) {
		for (var i in labels) {
			new this.ItemProgress(icons[i], labels[i]);
		}

		_move();
	});

	/**
	 * Add a description to the progress window
	 *
	 * <a> elements are turned into XUL links
	 */
	this.addDescription = _deferUntilWindowLoad(function addDescription(text) {
		var newHB = _progressWindow.document.createElement("hbox");
		newHB.setAttribute("class", "zotero-progress-item-hbox");
		var newDescription = _progressWindow.document.createElement("description");

		var parts = Zotero.Utilities.parseMarkup(text);
		for each(var part in parts) {
			if (part.type == 'text') {
				var elem = _progressWindow.document.createTextNode(part.text);
			}
			else if (part.type == 'link') {
				var elem = _progressWindow.document.createElement('label');
				elem.setAttribute('value', part.text);
				elem.setAttribute('class', 'zotero-text-link');
				for (var i in part.attributes) {
					elem.setAttribute(i, part.attributes[i]);
				}
			}

			newDescription.appendChild(elem);
		}

		newHB.appendChild(newDescription);
		_progressWindow.document.getElementById("zotero-progress-text-box").appendChild(newHB);

		_move();
	});

	/**
	 * Sets a timer to close the progress window. If a previous close timer was set,
	 * clears it.
	 * @param {Integer} ms The number of milliseconds to wait before closing the progress
	 *     window.
	 * @param {Boolean} [requireMouseOver] If true, wait until the mouse has touched the
	 *     window before closing.
	 */
	this.startCloseTimer = function startCloseTimer(ms, requireMouseOver) {
		if (_windowLoaded || _windowLoading) {
			if (requireMouseOver && !_mouseWasOver) {
				return;
			}

			if (_timeoutID) {
				_disableTimeout();
			}

			if (typeof ms != 'number') {
				ms = 2500;
			}

			_timeoutID = _progressWindow.setTimeout(_timeout, ms);
			_closing = true;
		}
	}

	/**
	 * Immediately closes the progress window if it is open.
	 */
	this.close = function close() {
		_disableTimeout();
		_windowLoaded = false;
		_windowLoading = false;
		Zotero.ProgressWindowSet.remove(_progressWindow);

		try {
			_progressWindow.close();
		} catch(ex) {}
	}

	/**
	 * Creates a new object representing a line in the progressWindow. This is the OO
	 * version of addLines() above.
	 */
	this.ItemProgress = _deferUntilWindowLoad(function(iconSrc, title, parentItemProgress) {
		this._itemText = _progressWindow.document.createElement("description");
		this._itemText.appendChild(_progressWindow.document.createTextNode(title));
		this._itemText.setAttribute("class", "zotero-progress-item-label");
		this._itemText.setAttribute("crop", "end");

		this._image = _progressWindow.document.createElement("hbox");
		this._image.setAttribute("class", "zotero-progress-item-icon");
		this._image.setAttribute("flex", 0);
		this._image.style.width = "16px";
		this._image.style.backgroundRepeat = "no-repeat";
		this.setIcon(iconSrc);

		this._hbox = _progressWindow.document.createElement("hbox");
		this._hbox.setAttribute("class", "zotero-progress-item-hbox");
		if(parentItemProgress) {
			this._hbox.style.marginLeft = "16px";
			this._hbox.zoteroIsChildItem;
		} else {
			this._hbox.setAttribute("parent", "true");
		}
		this._hbox.style.opacity = "0.5";

		this._hbox.appendChild(this._image);
		this._hbox.appendChild(this._itemText);

		var container = _progressWindow.document.getElementById("zotero-progress-text-box");
		if(parentItemProgress) {
			var nextItem = parentItemProgress._hbox.nextSibling;
			while(nextItem && nextItem.zoteroIsChildItem) {
				nextItem = nextItem.nextSibling;
			}
			container.insertBefore(this._hbox, nextItem);
		} else {
			container.appendChild(this._hbox);
		}

		_move();
	});

	/**
	 * Sets the current save progress for this item.
	 * @param {Integer} percent A percentage from 0 to 100.
	 */
	this.ItemProgress.prototype.setProgress = _deferUntilWindowLoad(function(percent) {
		if(percent != 0 && percent != 100) {
			// Indication of partial progress, so we will use the circular indicator
			this._image.style.backgroundImage = "url('chrome://zotero/skin/progress_arcs.png')";
			this._image.style.backgroundPosition = "-"+(Math.round(percent/100*nArcs)*16)+"px 0";
			this._hbox.style.opacity = percent/200+.5;
			this._hbox.style.filter = "alpha(opacity = "+(percent/2+50)+")";
		} else if(percent == 100) {
			this._image.style.backgroundImage = "url('"+this._iconSrc+"')";
			this._image.style.backgroundPosition = "";
			this._hbox.style.opacity = "1";
			this._hbox.style.filter = "";
		}
	});

	/**
	 * Sets the icon for this item.
	 * @param {Integer} percent A percentage from 0 to 100.
	 */
	this.ItemProgress.prototype.setIcon = _deferUntilWindowLoad(function(iconSrc) {
		this._image.style.backgroundImage = "url('"+iconSrc+"')";
		this._image.style.backgroundPosition = "";
		this._iconSrc = iconSrc;
	});

	/**
	 * Indicates that an error occurred saving this item.
	 */
	this.ItemProgress.prototype.setError = _deferUntilWindowLoad(function() {
		this._image.style.backgroundImage = "url('chrome://zotero/skin/cross.png')";
		this._image.style.backgroundPosition = "";
		this._itemText.style.color = "red";
		this._hbox.style.opacity = "1";
		this._hbox.style.filter = "";
	});

	function _onWindowLoaded() {
		_windowLoading = false;
		_windowLoaded = true;

		_move();

		// do things we delayed because the window was loading
		for(var i=0; i<_deferredUntilWindowLoad.length; i++) {
			_deferredUntilWindowLoad[i].apply(_deferredUntilWindowLoadThis[i],
				_deferredUntilWindowLoadArgs[i]);
		}
		_deferredUntilWindowLoad = [];
		_deferredUntilWindowLoadThis = [];
		_deferredUntilWindowLoadArgs = [];
	}

	function _move() {
		// sizeToContent() fails in FF3 with multiple lines
		// if we don't change the height
		_progressWindow.outerHeight = _progressWindow.outerHeight + 1;
		_progressWindow.sizeToContent();
		Zotero.ProgressWindowSet.tile(_progressWindow);
	}

	function _timeout() {
		self.close();	// could check to see if we're really supposed to close yet
						// (in case multiple scrapers are operating at once)
		_timeoutID = false;
	}

	function _disableTimeout() {
		// FIXME: to prevent errors from translator saving (Create New Item appears to still work)
		// This shouldn't be necessary, and mouseover isn't properly
		// causing the popup to remain
		try {
			_progressWindow.clearTimeout(_timeoutID);
		}
		catch (e) {}
		_timeoutID = false;
	}

	/*
	 * Disable the close timer when the mouse is over the window
	 */
	function _onMouseOver(e) {
		_mouseWasOver = true;
		_disableTimeout();
	}

	/**
	 * Start the close timer when the mouse leaves the window
	 *
	 * Note that this onmouseout doesn't work correctly on popups in Fx2,
	 * so 1) we have to calculate the window borders manually to avoid fading
	 * when the mouse is still over the box, and 2) this only does anything
	 * when the mouse is moved off of the browser window -- otherwise the close
	 * is triggered by onmousemove on appcontent in overlay.xul.
	 */
	function _onMouseOut(e) {
		// |this| refers to progressWindow's XUL window
		var top = this.screenY + (Zotero.isMac ? 22 : 0);
		if ((e.screenX >= this.screenX && e.screenX <= (this.screenX + this.outerWidth))
			&& (e.screenY >= top) && e.screenY <= (top + this.outerHeight)) {
				return;
		}
		if(_closing) self.startCloseTimer();
	}

	function _onMouseUp(e) {
		self.close();
	}

	/**
	 * Wraps a function to ensure it isn't called until the window is loaded
	 */
	function _deferUntilWindowLoad(fn) {
		return function() {
			if(_window.closed) return;

			if(_windowLoaded) {
				fn.apply(this, Array.prototype.slice.call(arguments));
			} else {
				_deferredUntilWindowLoad.push(fn);
				_deferredUntilWindowLoadThis.push(this);
				_deferredUntilWindowLoadArgs.push(Array.prototype.slice.call(arguments));
			}
		}
	}
}
<commit_msg>Fix saving snapshots<commit_after>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero 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, either version 3 of the License, or
    (at your option) any later version.

    Zotero 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 Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.ProgressWindowSet = new function() {
	this.add = add;
	this.tile = tile;
	this.remove = remove;
	this.updateTimers = updateTimers;

	var _progressWindows = [];

	const X_OFFSET = 25;
	const Y_OFFSET = 35;
	const Y_SEPARATOR = 12;
	const X_WINDOWLESS_OFFSET = 50;
	const Y_WINDOWLESS_OFFSET = 100;

	function add(progressWindow, instance) {
		_progressWindows.push({
			progressWindow: progressWindow,
			instance: instance
		});
	}

	function tile(progressWin) {
		var parent = progressWin.opener;
		var y_sub = null;

		for (var i=0; i<_progressWindows.length; i++) {
			var p = _progressWindows[i].progressWindow;

			// Skip progress windows from other windows
			if (p.opener != parent) {
				continue;
			}

			if (!y_sub) {
				y_sub = Y_OFFSET + p.outerHeight;
			}

			if (parent) {
				var right = parent.screenX + parent.outerWidth;
				var bottom = parent.screenY + parent.outerHeight;
			}
			else {
				var right = progressWin.screen.width + X_OFFSET - X_WINDOWLESS_OFFSET;
				var bottom = progressWin.screen.height + Y_OFFSET - Y_WINDOWLESS_OFFSET;
			}

			p.moveTo(right - p.outerWidth - X_OFFSET, bottom - y_sub);

			y_sub += p.outerHeight + Y_SEPARATOR;
		}
	}

	function remove(progressWin) {
		for (var i=0; i<_progressWindows.length; i++) {
			if (_progressWindows[i].progressWindow == progressWin) {
				_progressWindows.splice(i, 1);
			}
		}
	}

	function updateTimers() {
		if (!_progressWindows.length) {
			return;
		}

		for (var i=0; i<_progressWindows.length; i++) {
			// Pass |requireMouseOver| so that the window only closes
			// if the mouse was over it at some point
			_progressWindows[i].instance.startCloseTimer(null, true);
		}
	}
}

/*
 * Handles the display of a div showing progress in scraping, indexing, etc.
 *
 * Pass the active window into the constructor
 */
Zotero.ProgressWindow = function(_window){
	var self = this,
		_window = null,
		_progressWindow = null,
		_windowLoaded = false,
		_windowLoading = false,
		_timeoutID = false,
		_closing = false,
		_mouseWasOver = false,
		_deferredUntilWindowLoad = [],
		_deferredUntilWindowLoadThis = [],
		_deferredUntilWindowLoadArgs = [];

	/**
	 * Shows the progress window
	 */
	this.show = function show() {
		if(_windowLoading || _windowLoaded) {	// already loading or loaded
			return false;
		}

		var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"].
					getService(Components.interfaces.nsIWindowWatcher);

		if (!_window){
			_window = Components.classes["@mozilla.org/appshell/window-mediator;1"]
				.getService(Components.interfaces.nsIWindowMediator)
				.getMostRecentWindow("navigator:browser");
		}

		if (_window) {
			_progressWindow = _window.openDialog("chrome://zotero/content/progressWindow.xul",
				"", "chrome,dialog=no,titlebar=no,popup=yes");
		}
		else {
			_progressWindow = ww.openWindow(null, "chrome://zotero/content/progressWindow.xul",
				"", "chrome,dialog=no,titlebar=no,popup=yes", null);
		}
		_progressWindow.addEventListener("load", _onWindowLoaded, false);
		_progressWindow.addEventListener("mouseover", _onMouseOver, false);
		_progressWindow.addEventListener("mouseout", _onMouseOut, false);
		_progressWindow.addEventListener("mouseup", _onMouseUp, false);

		_windowLoading = true;

		Zotero.ProgressWindowSet.add(_progressWindow, this);

		return true;
	}

	/**
	 * Changes the "headline" shown at the top of the progress window
	 */
	this.changeHeadline = _deferUntilWindowLoad(function changeHeadline(text, icon, postText) {
		var doc = _progressWindow.document,
			headline = doc.getElementById("zotero-progress-text-headline");
		while(headline.hasChildNodes()) headline.removeChild(headline.firstChild);

		var preNode = doc.createElement("label");
		preNode.setAttribute("value", text);
		preNode.setAttribute("crop", "end");
		headline.appendChild(preNode);

		if(icon) {
			var img = doc.createElement("image");
			img.width = 16;
			img.height = 16;
			img.setAttribute("src", icon);
			headline.appendChild(img);
		}

		if(postText) {
			var postNode = doc.createElement("label");
			postNode.style.marginLeft = 0;
			postNode.setAttribute("value", " "+postText);
			postNode.setAttribute("crop", "end");
			postNode.setAttribute("flex", "1");
			headline.appendChild(postNode);
		}
	});

	/**
	 * Adds a line to the progress window with the specified icon
	 */
	this.addLines = _deferUntilWindowLoad(function addLines(labels, icons) {
		if(typeof labels === "object" && typeof icons === "object") {
			for (var i in labels) {
				new this.ItemProgress(icons[i], labels[i]);
			}
		} else {
			new this.ItemProgress(icons, labels);
		}

		_move();
	});

	/**
	 * Add a description to the progress window
	 *
	 * <a> elements are turned into XUL links
	 */
	this.addDescription = _deferUntilWindowLoad(function addDescription(text) {
		var newHB = _progressWindow.document.createElement("hbox");
		newHB.setAttribute("class", "zotero-progress-item-hbox");
		var newDescription = _progressWindow.document.createElement("description");

		var parts = Zotero.Utilities.parseMarkup(text);
		for each(var part in parts) {
			if (part.type == 'text') {
				var elem = _progressWindow.document.createTextNode(part.text);
			}
			else if (part.type == 'link') {
				var elem = _progressWindow.document.createElement('label');
				elem.setAttribute('value', part.text);
				elem.setAttribute('class', 'zotero-text-link');
				for (var i in part.attributes) {
					elem.setAttribute(i, part.attributes[i]);
				}
			}

			newDescription.appendChild(elem);
		}

		newHB.appendChild(newDescription);
		_progressWindow.document.getElementById("zotero-progress-text-box").appendChild(newHB);

		_move();
	});

	/**
	 * Sets a timer to close the progress window. If a previous close timer was set,
	 * clears it.
	 * @param {Integer} ms The number of milliseconds to wait before closing the progress
	 *     window.
	 * @param {Boolean} [requireMouseOver] If true, wait until the mouse has touched the
	 *     window before closing.
	 */
	this.startCloseTimer = function startCloseTimer(ms, requireMouseOver) {
		if (_windowLoaded || _windowLoading) {
			if (requireMouseOver && !_mouseWasOver) {
				return;
			}

			if (_timeoutID) {
				_disableTimeout();
			}

			if (typeof ms != 'number') {
				ms = 2500;
			}

			_timeoutID = _progressWindow.setTimeout(_timeout, ms);
			_closing = true;
		}
	}

	/**
	 * Immediately closes the progress window if it is open.
	 */
	this.close = function close() {
		_disableTimeout();
		_windowLoaded = false;
		_windowLoading = false;
		Zotero.ProgressWindowSet.remove(_progressWindow);

		try {
			_progressWindow.close();
		} catch(ex) {}
	}

	/**
	 * Creates a new object representing a line in the progressWindow. This is the OO
	 * version of addLines() above.
	 */
	this.ItemProgress = _deferUntilWindowLoad(function(iconSrc, title, parentItemProgress) {
		this._itemText = _progressWindow.document.createElement("description");
		this._itemText.appendChild(_progressWindow.document.createTextNode(title));
		this._itemText.setAttribute("class", "zotero-progress-item-label");
		this._itemText.setAttribute("crop", "end");

		this._image = _progressWindow.document.createElement("hbox");
		this._image.setAttribute("class", "zotero-progress-item-icon");
		this._image.setAttribute("flex", 0);
		this._image.style.width = "16px";
		this._image.style.backgroundRepeat = "no-repeat";
		this.setIcon(iconSrc);

		this._hbox = _progressWindow.document.createElement("hbox");
		this._hbox.setAttribute("class", "zotero-progress-item-hbox");
		if(parentItemProgress) {
			this._hbox.style.marginLeft = "16px";
			this._hbox.zoteroIsChildItem;
		} else {
			this._hbox.setAttribute("parent", "true");
		}
		this._hbox.style.opacity = "0.5";

		this._hbox.appendChild(this._image);
		this._hbox.appendChild(this._itemText);

		var container = _progressWindow.document.getElementById("zotero-progress-text-box");
		if(parentItemProgress) {
			var nextItem = parentItemProgress._hbox.nextSibling;
			while(nextItem && nextItem.zoteroIsChildItem) {
				nextItem = nextItem.nextSibling;
			}
			container.insertBefore(this._hbox, nextItem);
		} else {
			container.appendChild(this._hbox);
		}

		_move();
	});

	/**
	 * Sets the current save progress for this item.
	 * @param {Integer} percent A percentage from 0 to 100.
	 */
	this.ItemProgress.prototype.setProgress = _deferUntilWindowLoad(function(percent) {
		if(percent != 0 && percent != 100) {
			// Indication of partial progress, so we will use the circular indicator
			this._image.style.backgroundImage = "url('chrome://zotero/skin/progress_arcs.png')";
			this._image.style.backgroundPosition = "-"+(Math.round(percent/100*nArcs)*16)+"px 0";
			this._hbox.style.opacity = percent/200+.5;
			this._hbox.style.filter = "alpha(opacity = "+(percent/2+50)+")";
		} else if(percent == 100) {
			this._image.style.backgroundImage = "url('"+this._iconSrc+"')";
			this._image.style.backgroundPosition = "";
			this._hbox.style.opacity = "1";
			this._hbox.style.filter = "";
		}
	});

	/**
	 * Sets the icon for this item.
	 * @param {Integer} percent A percentage from 0 to 100.
	 */
	this.ItemProgress.prototype.setIcon = _deferUntilWindowLoad(function(iconSrc) {
		this._image.style.backgroundImage = "url('"+iconSrc+"')";
		this._image.style.backgroundPosition = "";
		this._iconSrc = iconSrc;
	});

	/**
	 * Indicates that an error occurred saving this item.
	 */
	this.ItemProgress.prototype.setError = _deferUntilWindowLoad(function() {
		this._image.style.backgroundImage = "url('chrome://zotero/skin/cross.png')";
		this._image.style.backgroundPosition = "";
		this._itemText.style.color = "red";
		this._hbox.style.opacity = "1";
		this._hbox.style.filter = "";
	});

	function _onWindowLoaded() {
		_windowLoading = false;
		_windowLoaded = true;

		_move();

		// do things we delayed because the window was loading
		for(var i=0; i<_deferredUntilWindowLoad.length; i++) {
			_deferredUntilWindowLoad[i].apply(_deferredUntilWindowLoadThis[i],
				_deferredUntilWindowLoadArgs[i]);
		}
		_deferredUntilWindowLoad = [];
		_deferredUntilWindowLoadThis = [];
		_deferredUntilWindowLoadArgs = [];
	}

	function _move() {
		// sizeToContent() fails in FF3 with multiple lines
		// if we don't change the height
		_progressWindow.outerHeight = _progressWindow.outerHeight + 1;
		_progressWindow.sizeToContent();
		Zotero.ProgressWindowSet.tile(_progressWindow);
	}

	function _timeout() {
		self.close();	// could check to see if we're really supposed to close yet
						// (in case multiple scrapers are operating at once)
		_timeoutID = false;
	}

	function _disableTimeout() {
		// FIXME: to prevent errors from translator saving (Create New Item appears to still work)
		// This shouldn't be necessary, and mouseover isn't properly
		// causing the popup to remain
		try {
			_progressWindow.clearTimeout(_timeoutID);
		}
		catch (e) {}
		_timeoutID = false;
	}

	/*
	 * Disable the close timer when the mouse is over the window
	 */
	function _onMouseOver(e) {
		_mouseWasOver = true;
		_disableTimeout();
	}

	/**
	 * Start the close timer when the mouse leaves the window
	 *
	 * Note that this onmouseout doesn't work correctly on popups in Fx2,
	 * so 1) we have to calculate the window borders manually to avoid fading
	 * when the mouse is still over the box, and 2) this only does anything
	 * when the mouse is moved off of the browser window -- otherwise the close
	 * is triggered by onmousemove on appcontent in overlay.xul.
	 */
	function _onMouseOut(e) {
		// |this| refers to progressWindow's XUL window
		var top = this.screenY + (Zotero.isMac ? 22 : 0);
		if ((e.screenX >= this.screenX && e.screenX <= (this.screenX + this.outerWidth))
			&& (e.screenY >= top) && e.screenY <= (top + this.outerHeight)) {
				return;
		}
		if(_closing) self.startCloseTimer();
	}

	function _onMouseUp(e) {
		self.close();
	}

	/**
	 * Wraps a function to ensure it isn't called until the window is loaded
	 */
	function _deferUntilWindowLoad(fn) {
		return function() {
			if(_window && _window.closed) return;

			if(_windowLoaded) {
				fn.apply(this, Array.prototype.slice.call(arguments));
			} else {
				_deferredUntilWindowLoad.push(fn);
				_deferredUntilWindowLoadThis.push(this);
				_deferredUntilWindowLoadArgs.push(Array.prototype.slice.call(arguments));
			}
		}
	}
}
<|endoftext|>


<commit_before>define(
  [
    'underscore',
    'dispatchers/Dispatcher',
    'stores/Store',
    'collections/InstanceCollection',
    'models/Instance',
    'constants/InstanceConstants',
    'controllers/NotificationController',
    'stores/IdentityStore',
    'actions/ProjectActions',
    'q'
  ],
  function (_, Dispatcher, Store, InstanceCollection, Instance, InstanceConstants, NotificationController, IdentityStore, ProjectActions, Q) {

    var _instances = new InstanceCollection();
    var _isFetching = false;
    var pollingFrequency = 10*1000;

    //
    // CRUD Operations
    //

    var fetchInstancesFor = function (providerId, identityId) {
      var defer = Q.defer();

      var instances = new InstanceCollection(null, {
        provider_id: providerId,
        identity_id: identityId
      });

      // make sure promise returns the right instances collection
      // for when this function is called multiple times
      (function(instances, defer){
        instances.fetch().done(function(){
          defer.resolve(instances);
        });
      })(instances, defer);

      return defer.promise;
    };

    var fetchInstances = function (identities) {
      if(!_isFetching && identities) {
        _isFetching = true;

        // return an array of promises (one for each volume collection being fetched)
        var promises = identities.map(function (identity) {
          var providerId = identity.get('provider_id');
          var identityId = identity.get('id');
          return fetchInstancesFor(providerId, identityId);
        });

        // When all instance collections are fetched...
        Q.all(promises).done(function (instanceCollections) {
          // Combine results into a single volume collection
          var instances = new InstanceCollection();
          for (var i = 0; i < instanceCollections.length; i++) {
            instances.add(instanceCollections[i].toJSON());
          }

          // Start polling for any instances that are in transition states
          instances.forEach(function(instance){
            if(!instance.get('state').isInFinalState()){
              pollUntilBuildIsFinished(instance);
            }
          });

          // Save the results to local cache
          _isFetching = false;
          _instances = instances;
          InstanceStore.emitChange();
        });
      }
    };

    var update = function(instance){
      instance.save({
        name: instance.get('name'),
        tags: instance.get('tags')
      }, {
        patch: true
      }).done(function(){
        var successMessage = "Instance " + instance.get('name') + " updated.";
        //NotificationController.success(successMessage);
        InstanceStore.emitChange();
      }).fail(function(){
        var failureMessage = "Error updating Instance " + instance.get('name') + ".";
        NotificationController.error(failureMessage);
        InstanceStore.emitChange();
      });
    };

    var addTagToInstance = function(tag, instance){
      var instanceTags = instance.get('tags');
      instanceTags.push(tag.get('name'));
      instance.save({
        tags: instanceTags
      }, {
        patch: true
      }).done(function(){
        InstanceStore.emitChange();
      }).fail(function(){
        var failureMessage = "Error adding tag to Instance";
        NotificationController.error(failureMessage);
        InstanceStore.emitChange();
      });
    };

    //
    // Instance Actions
    //

    var suspend = function(instance){
      instance.suspend({
        success: function (model) {
          //NotificationController.success("Success", "Your instance is now suspended");
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error("Error", "Your instance could not be suspended");
          InstanceStore.emitChange();
        }
      });
    };

    var resume = function(instance){
      instance.resume({
        success: function (model) {
          //NotificationController.success("Success", "Your instance is resuming");
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error("Error", "Your instance could not be resumed");
          InstanceStore.emitChange();
        }
      });
    };

    var stop = function(instance){
      instance.stop({
        success: function (model) {
          //NotificationController.success('Stop Instance', 'Instance successfully stopped');
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be stopped');
          InstanceStore.emitChange();
        }
      });
    };

    var start = function(instance){
      instance.start({
        success: function (model) {
          //NotificationController.success('Start Instance', 'Instance successfully started');
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be started');
          InstanceStore.emitChange();
        }
      });
    };

    var terminate = function(instance){
      instance.destroy({
        success: function (model) {
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be terminated');
          _instances.add(instance);
          InstanceStore.emitChange();
        }
      });
      _instances.remove(instance);
    };

    var launch = function(identity, machineId, sizeId, instanceName, project){
      var instance = new Instance({
        identity: {
          id: identity.id,
          provider: identity.get('provider_id')
        },
        status: "build - scheduling",
        projects: [project.id]
      }, {parse: true});

      var params = {
        machine_alias: machineId,
        size_alias: sizeId,
        name: instanceName
      };

      instance.save(params, {
        success: function (model) {
          NotificationController.success('Launch Instance', 'Instance successfully launched');
          pollUntilBuildIsFinished(instance);
          ProjectActions.addItemToProject(project, instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be launched');
          _instances.remove(instance);
          InstanceStore.emitChange();
        }
      });
      _instances.add(instance);
    };

    //
    // Polling Functions
    //

    var _instancesBuilding = [];
    var pollUntilBuildIsFinished = function(instance){
      if(_instancesBuilding.indexOf(instance) < 0) {
        _instancesBuilding.push(instance);
        fetchAndRemoveIfFinished(instance);
      }
    };

    // Poll
    var pollNowUntilBuildIsFinished = function(instance){
      if(_instancesBuilding.indexOf(instance) < 0) {
        _instancesBuilding.push(instance);
        fetchNowAndRemoveIfFinished(instance);
      }
    };

    var fetchAndRemoveIfFinished = function(instance){
      setTimeout(function(){
        instance.fetch().done(function(){
          var index = _instancesBuilding.indexOf(instance);
          if(instance.get('state').isInFinalState()){
            _instancesBuilding.slice(index, 1);
          }else{
            fetchAndRemoveIfFinished(instance);
          }
          InstanceStore.emitChange();
        });
      }, pollingFrequency);
    };

    var fetchNowAndRemoveIfFinished = function(instance){
      instance.fetch().done(function(){
        var index = _instancesBuilding.indexOf(instance);
        if(instance.get('state').isInFinalState()){
          _instancesBuilding.slice(index, 1);
        }else{
          fetchAndRemoveIfFinished(instance);
        }
        InstanceStore.emitChange();
      });
    };

    //
    // Instance Store
    //

    var InstanceStore = {

      // until instances have their own endpoint at /instances
      // we need to either fetch them using identities or the
      // information we get from projects
      getAll: function (projects) {
        if(!projects) throw new Error("Must supply projects");

        projects.each(function(project){
          this.getInstancesInProject(project);
        }.bind(this));

        return _instances;
      },

      // getAll: function () {
      //   if(!_instances) {
      //     var identities = IdentityStore.getAll();
      //     if(identities) {
      //       fetchInstances(identities);
      //     }
      //   }
      //   return _instances;
      // },

      // get: function (instanceId) {
      //   if(!_instances) {
      //     var identities = IdentityStore.getAll();
      //     if(identities) {
      //       fetchInstances(identities);
      //     }
      //   } else {
      //     return _instances.get(instanceId);
      //   }
      // },

      getInstanceInProject: function(project, instanceId){
        var instances = this.getInstancesInProject(project);
        var instance = instances.get(instanceId);
        if(!instance){
          NotificationController.error("Instance not in project", "The instance could not be found in the project");
        }
        return instance;
      },

      getInstancesInProject: function (project) {

        var projectInstanceArray = project.get('instances').map(function(instanceData){
          // todo: we're converting into an instance object here so we can use
          // id instead of alias for consistency. Eventually all alias attributes
          // need to be renamed id and then we can create the object only if
          // the id isn't in the existing map.
          var instance = new Instance(instanceData, {parse: true});
          var existingInstance = _instances.get(instance.id);

          if(existingInstance){
            instance = existingInstance;
          }else{
            _instances.push(instance);
            pollNowUntilBuildIsFinished(instance);
          }

          return instance;
        });

        var projectInstances = new InstanceCollection(projectInstanceArray);
        return projectInstances;
      }

    };

    Dispatcher.register(function (payload) {
      var action = payload.action;

      switch (action.actionType) {
        case InstanceConstants.INSTANCE_UPDATE:
          update(action.instance);
          break;

        case InstanceConstants.INSTANCE_SUSPEND:
          suspend(action.instance);
          break;

        case InstanceConstants.INSTANCE_RESUME:
          resume(action.instance);
          break;

        case InstanceConstants.INSTANCE_STOP:
          stop(action.instance);
          break;

        case InstanceConstants.INSTANCE_START:
          start(action.instance);
          break;

        case InstanceConstants.INSTANCE_TERMINATE:
          terminate(action.instance);
          break;

        case InstanceConstants.INSTANCE_LAUNCH:
          launch(action.identity, action.machineId, action.sizeId, action.instanceName, action.project);
          break;

        case InstanceConstants.INSTANCE_ADD_TAG:
          addTagToInstance(action.tag, action.instance);
          break;

        default:
          return true;
      }

      InstanceStore.emitChange();

      return true;
    });

    _.extend(InstanceStore, Store);

    return InstanceStore;
  });
<commit_msg>add instances to project optimistically<commit_after>define(
  [
    'underscore',
    'dispatchers/Dispatcher',
    'stores/Store',
    'collections/InstanceCollection',
    'models/Instance',
    'constants/InstanceConstants',
    'controllers/NotificationController',
    'stores/IdentityStore',
    'actions/ProjectActions',
    'q'
  ],
  function (_, Dispatcher, Store, InstanceCollection, Instance, InstanceConstants, NotificationController, IdentityStore, ProjectActions, Q) {

    var _instances = new InstanceCollection();
    var _isFetching = false;
    var pollingFrequency = 10*1000;
    var _pendingProjectInstances = {};

    //
    // CRUD Operations
    //

    var fetchInstancesFor = function (providerId, identityId) {
      var defer = Q.defer();

      var instances = new InstanceCollection(null, {
        provider_id: providerId,
        identity_id: identityId
      });

      // make sure promise returns the right instances collection
      // for when this function is called multiple times
      (function(instances, defer){
        instances.fetch().done(function(){
          defer.resolve(instances);
        });
      })(instances, defer);

      return defer.promise;
    };

    var fetchInstances = function (identities) {
      if(!_isFetching && identities) {
        _isFetching = true;

        // return an array of promises (one for each volume collection being fetched)
        var promises = identities.map(function (identity) {
          var providerId = identity.get('provider_id');
          var identityId = identity.get('id');
          return fetchInstancesFor(providerId, identityId);
        });

        // When all instance collections are fetched...
        Q.all(promises).done(function (instanceCollections) {
          // Combine results into a single volume collection
          var instances = new InstanceCollection();
          for (var i = 0; i < instanceCollections.length; i++) {
            instances.add(instanceCollections[i].toJSON());
          }

          // Start polling for any instances that are in transition states
          instances.forEach(function(instance){
            if(!instance.get('state').isInFinalState()){
              pollUntilBuildIsFinished(instance);
            }
          });

          // Save the results to local cache
          _isFetching = false;
          _instances = instances;
          InstanceStore.emitChange();
        });
      }
    };

    var update = function(instance){
      instance.save({
        name: instance.get('name'),
        tags: instance.get('tags')
      }, {
        patch: true
      }).done(function(){
        var successMessage = "Instance " + instance.get('name') + " updated.";
        //NotificationController.success(successMessage);
        InstanceStore.emitChange();
      }).fail(function(){
        var failureMessage = "Error updating Instance " + instance.get('name') + ".";
        NotificationController.error(failureMessage);
        InstanceStore.emitChange();
      });
    };

    var addTagToInstance = function(tag, instance){
      var instanceTags = instance.get('tags');
      instanceTags.push(tag.get('name'));
      instance.save({
        tags: instanceTags
      }, {
        patch: true
      }).done(function(){
        InstanceStore.emitChange();
      }).fail(function(){
        var failureMessage = "Error adding tag to Instance";
        NotificationController.error(failureMessage);
        InstanceStore.emitChange();
      });
    };

    //
    // Instance Actions
    //

    var suspend = function(instance){
      instance.suspend({
        success: function (model) {
          //NotificationController.success("Success", "Your instance is now suspended");
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error("Error", "Your instance could not be suspended");
          InstanceStore.emitChange();
        }
      });
    };

    var resume = function(instance){
      instance.resume({
        success: function (model) {
          //NotificationController.success("Success", "Your instance is resuming");
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error("Error", "Your instance could not be resumed");
          InstanceStore.emitChange();
        }
      });
    };

    var stop = function(instance){
      instance.stop({
        success: function (model) {
          //NotificationController.success('Stop Instance', 'Instance successfully stopped');
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be stopped');
          InstanceStore.emitChange();
        }
      });
    };

    var start = function(instance){
      instance.start({
        success: function (model) {
          //NotificationController.success('Start Instance', 'Instance successfully started');
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be started');
          InstanceStore.emitChange();
        }
      });
    };

    var terminate = function(instance){
      instance.destroy({
        success: function (model) {
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be terminated');
          _instances.add(instance);
          InstanceStore.emitChange();
        }
      });
      _instances.remove(instance);
    };

    var launch = function(identity, machineId, sizeId, instanceName, options){
      options = options || {};

      var instance = new Instance({
        identity: {
          id: identity.id,
          provider: identity.get('provider_id')
        },
        status: "build - scheduling"
      }, {parse: true});

      var params = {
        machine_alias: machineId,
        size_alias: sizeId,
        name: instanceName
      };

      if(options.afterCreate) options.afterCreate(instance);

      instance.save(params, {
        success: function (model) {
          if(options.afterSave) options.afterSave(instance);
          pollUntilBuildIsFinished(instance);
          InstanceStore.emitChange();
        },
        error: function (response) {
          NotificationController.error('Error', 'Instance could not be launched');
          if(options.afterSaveError) options.afterSaveError(instance);
          _instances.remove(instance);
          InstanceStore.emitChange();
        }
      });
      _instances.add(instance);
    };

    var launch_AddToProject = function(identity, machineId, sizeId, instanceName, project){
      launch(identity, machineId, sizeId, instanceName, {
        afterCreate: function(instance){
          _pendingProjectInstances[project.id] = _pendingProjectInstances[project.id] || new InstanceCollection();
          _pendingProjectInstances[project.id].add(instance);
        },
        afterSave: function(instance){
          _pendingProjectInstances[project.id].remove(instance);
          ProjectActions.addItemToProject(project, instance);
        },
        afterSaveError: function(instance){
          _pendingProjectInstances[project.id].remove(instance);
        }
      })
    };

    //
    // Polling Functions
    //

    var _instancesBuilding = [];
    var pollUntilBuildIsFinished = function(instance){
      if(_instancesBuilding.indexOf(instance) < 0) {
        _instancesBuilding.push(instance);
        fetchAndRemoveIfFinished(instance);
      }
    };

    // Poll
    var pollNowUntilBuildIsFinished = function(instance){
      if(_instancesBuilding.indexOf(instance) < 0) {
        _instancesBuilding.push(instance);
        fetchNowAndRemoveIfFinished(instance);
      }
    };

    var fetchAndRemoveIfFinished = function(instance){
      setTimeout(function(){
        instance.fetch().done(function(){
          var index = _instancesBuilding.indexOf(instance);
          if(instance.get('state').isInFinalState()){
            _instancesBuilding.slice(index, 1);
          }else{
            fetchAndRemoveIfFinished(instance);
          }
          InstanceStore.emitChange();
        });
      }, pollingFrequency);
    };

    var fetchNowAndRemoveIfFinished = function(instance){
      instance.fetch().done(function(){
        var index = _instancesBuilding.indexOf(instance);
        if(instance.get('state').isInFinalState()){
          _instancesBuilding.slice(index, 1);
        }else{
          fetchAndRemoveIfFinished(instance);
        }
        InstanceStore.emitChange();
      });
    };

    //
    // Instance Store
    //

    var InstanceStore = {

      // until instances have their own endpoint at /instances
      // we need to either fetch them using identities or the
      // information we get from projects
      getAll: function (projects) {
        if(!projects) throw new Error("Must supply projects");

        projects.each(function(project){
          this.getInstancesInProject(project);
        }.bind(this));

        return _instances;
      },

      // getAll: function () {
      //   if(!_instances) {
      //     var identities = IdentityStore.getAll();
      //     if(identities) {
      //       fetchInstances(identities);
      //     }
      //   }
      //   return _instances;
      // },

      // get: function (instanceId) {
      //   if(!_instances) {
      //     var identities = IdentityStore.getAll();
      //     if(identities) {
      //       fetchInstances(identities);
      //     }
      //   } else {
      //     return _instances.get(instanceId);
      //   }
      // },

      getInstanceInProject: function(project, instanceId){
        var instances = this.getInstancesInProject(project);
        var instance = instances.get(instanceId);
        if(!instance){
          NotificationController.error("Instance not in project", "The instance could not be found in the project");
        }
        return instance;
      },

      getInstancesInProject: function (project) {

        var projectInstanceArray = project.get('instances').map(function(instanceData){
          // todo: we're converting into an instance object here so we can use
          // id instead of alias for consistency. Eventually all alias attributes
          // need to be renamed id and then we can create the object only if
          // the id isn't in the existing map.
          var instance = new Instance(instanceData, {parse: true});
          var existingInstance = _instances.get(instance.id);

          if(existingInstance){
            instance = existingInstance;
          }else{
            _instances.push(instance);
            pollNowUntilBuildIsFinished(instance);
          }

          return instance;
        });

        // Add any pending instances to the result set
        var pendingProjectInstances = _pendingProjectInstances[project.id];
        if(pendingProjectInstances){
          projectInstanceArray = projectInstanceArray.concat(pendingProjectInstances.models);
        }

        return new InstanceCollection(projectInstanceArray);
      }

    };

    Dispatcher.register(function (payload) {
      var action = payload.action;

      switch (action.actionType) {
        case InstanceConstants.INSTANCE_UPDATE:
          update(action.instance);
          break;

        case InstanceConstants.INSTANCE_SUSPEND:
          suspend(action.instance);
          break;

        case InstanceConstants.INSTANCE_RESUME:
          resume(action.instance);
          break;

        case InstanceConstants.INSTANCE_STOP:
          stop(action.instance);
          break;

        case InstanceConstants.INSTANCE_START:
          start(action.instance);
          break;

        case InstanceConstants.INSTANCE_TERMINATE:
          terminate(action.instance);
          break;

        case InstanceConstants.INSTANCE_LAUNCH:
          launch_AddToProject(action.identity, action.machineId, action.sizeId, action.instanceName, action.project);
          break;

        case InstanceConstants.INSTANCE_ADD_TAG:
          addTagToInstance(action.tag, action.instance);
          break;

        default:
          return true;
      }

      InstanceStore.emitChange();

      return true;
    });

    _.extend(InstanceStore, Store);

    return InstanceStore;
  });
<|endoftext|>