text: /*
   * Copyright (C) 2014 United States Government as represented by the Administrator of the
   * National Aeronautics and Space Administration. All Rights Reserved.
   */
  /**
   * @exports Tile
   * @version $Id: Tile.js 3125 2015-05-29 14:43:25Z tgaskins $
   */
  define([
          '../error/ArgumentError',
          '../geom/BoundingBox',
          '../util/Logger',
          '../geom/Sector',
          '../geom/Vec3',
          '../util/WWUtil'
      ],
      function (ArgumentError,
                BoundingBox,
                Logger,
                Sector,
                Vec3,
                WWUtil) {
          "use strict";
  
          /**
           * Constructs a tile for a specified sector, level, row and column.
           * @alias Tile
           * @constructor
           * @classdesc Represents a tile of terrain or imagery.
           * Provides a base class for texture tiles used by tiled image layers and elevation tiles used by elevation models.
           * Applications typically do not interact with this class.
           * @param {Sector} sector The sector represented by this tile.
           * @param {Level} level This tile's level in a tile pyramid.
           * @param {Number} row This tile's row in the specified level in a tile pyramid.
           * @param {Number} column This tile's column in the specified level in a tile pyramid.
           * @throws {ArgumentError} If the specified sector or level is null or undefined or the row or column arguments
           * are less than zero.
           */
          var Tile = function (sector, level, row, column) {
              if (!sector) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor", "missingSector"));
              }
  
              if (!level) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
                          "The specified level is null or undefined."));
              }
  
              if (row < 0 || column < 0) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
                          "The specified row or column is less than zero."));
              }
  
              /**
               * The sector represented by this tile.
               * @type {Sector}
               * @readonly
               */
              this.sector = sector;
  
              /**
               * The level at which this tile lies in a tile pyramid.
               * @type {Number}
               * @readonly
               */
              this.level = level;
  
              /**
               * The row in this tile's level in which this tile lies in a tile pyramid.
               * @type {Number}
               * @readonly
               */
              this.row = row;
  
              /**
               * The column in this tile's level in which this tile lies in a tile pyramid.
               * @type {Number}
               * @readonly
               */
              this.column = column;
  
              /**
               * The width in pixels or cells of this tile's associated resource.
               * @type {Number}
               */
              this.tileWidth = level.tileWidth;
  
              /**
               * The height in pixels or cells of this tile's associated resource.
               * @type {Number}
               */
              this.tileHeight = level.tileHeight;
  
              /**
               * The size in radians of pixels or cells of this tile's associated resource.
               * @type {Number}
               */
              this.texelSize = level.texelSize;
  
              /**
               * A key that uniquely identifies this tile within a level set.
               * @type {String}
               * @readonly
               */
              this.tileKey = level.levelNumber.toString() + "." + row.toString() + "." + column.toString();
  
              /**
               * The Cartesian bounding box of this tile.
               * @type {BoundingBox}
               */
              this.extent = null;
  
              /**
               * The tile's local origin in model coordinates. Any model coordinate points associates with the tile
               * should be relative to this point.
               * @type {Vec3}
               */
              this.referencePoint = null;
  
              /**
               * This tile's opacity.
               * @type {Number}
               * @default 1
               */
              this.opacity = 1;
  
              // Internal use only. Intentionally not documented.
              this.samplePoints = null;
  
              // Internal use only. Intentionally not documented.
              this.sampleElevations = null;
  
              // Internal use only. Intentionally not documented.
              this.updateTimestamp = null;
  
              // Internal use only. Intentionally not documented.
              this.updateVerticalExaggeration = null;
  
              // Internal use only. Intentionally not documented.
              this.updateGlobeStateKey = null;
          };
  
          /**
           * Indicates whether this tile is equivalent to a specified tile.
           * @param {Tile} that The tile to check equivalence with.
           * @returns {boolean} true if this tile is equivalent to the specified one, false if
           * they are not equivalent or the specified tile is null or undefined.
           */
          Tile.prototype.isEqual = function (that) {
              if (!that)
                  return false;
  
              if (!that.tileKey)
                  return false;
  
              return this.tileKey == that.tileKey;
          };
  
          /**
           * Returns the size of this tile in bytes.
           * @returns {Number} The size of this tile in bytes.
           */
          Tile.prototype.size = function () {
              return 4 // child pointer
                  + (4 + 32) // sector
                  + 4 //level pointer (the level is common to the layer or tessellator so is not included here)
                  + 8 // row and column
                  + 8 // texel size
                  + (4 + 32) // reference point
                  + (4 + 676) // bounding box
                  + 8 // min and max height
                  + (4 + 32) // nearest point
                  + 8; // extent timestamp and vertical exaggeration
          };
  
          /**
           * Computes an approximate distance from this tile to a specified vector.
           * @param {Vec3} vector The vector to compute the distance to.
           * @returns {number} The distance between this tile and the vector.
           * @throws {ArgumentError} If the specified vector is null or undefined.
           */
          Tile.prototype.distanceTo = function (vector) {
              if (!vector) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "distanceTo", "missingVector"));
              }
  
              var px = vector[0], py = vector[1], pz = vector[2],
                  dx, dy, dz,
                  points = this.samplePoints,
                  distance = Number.POSITIVE_INFINITY;
  
              for (var i = 0, len = points.length; i < len; i += 3) {
                  dx = px - points[i];
                  dy = py - points[i + 1];
                  dz = pz - points[i + 2];
                  distance = Math.min(distance, dx * dx + dy * dy + dz * dz); // minimum squared distance
              }
  
              return Math.sqrt(distance);
          };
  
          /**
           * Returns the four children formed by subdividing this tile.
           * @param {Level} level The level of the children.
           * @param {TileFactory} tileFactory The tile factory to use to create the children.
           * @returns {Tile[]} An array containing the four child tiles.
           * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
           */
          Tile.prototype.subdivide = function (level, tileFactory) {
              if (!level) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
                          "The specified level is null or undefined."));
              }
  
              if (!tileFactory) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
                          "The specified tile factory is null or undefined."));
              }
  
              var latMin = this.sector.minLatitude,
                  latMax = this.sector.maxLatitude,
                  latMid = this.sector.centroidLatitude(),
  
                  lonMin = this.sector.minLongitude,
                  lonMax = this.sector.maxLongitude,
                  lonMid = this.sector.centroidLongitude(),
  
                  subRow,
                  subCol,
                  childSector,
                  children = [];
  
              subRow = 2 * this.row;
              subCol = 2 * this.column;
              childSector = new Sector(latMin, latMid, lonMin, lonMid);
              children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  
              subRow = 2 * this.row;
              subCol = 2 * this.column + 1;
              childSector = new Sector(latMin, latMid, lonMid, lonMax);
              children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  
              subRow = 2 * this.row + 1;
              subCol = 2 * this.column;
              childSector = new Sector(latMid, latMax, lonMin, lonMid);
              children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  
              subRow = 2 * this.row + 1;
              subCol = 2 * this.column + 1;
              childSector = new Sector(latMid, latMax, lonMid, lonMax);
              children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  
              return children;
          };
  
          /**
           * Returns the four children formed by subdividing this tile, drawing those children from a specified cache
           * if they exist there.
           * @param {Level} level The level of the children.
           * @param {TileFactory} tileFactory The tile factory to use to create the children.
           * @param {MemoryCache} cache A memory cache that may contain pre-existing child tiles. If non-null, the
           * cache is checked for a child collection prior to creating that tile. If one exists
           * in the cache it is returned rather than creating a new collection of children. If a new collection is
           * created, it is added to the cache.
           * @returns {Tile[]} An array containing the four tiles.
           * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
           */
          Tile.prototype.subdivideToCache = function (level, tileFactory, cache) {
              if (!level) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
                          "The specified level is null or undefined."));
              }
  
              if (!tileFactory) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
                          "The specified tile factory is null or undefined."));
              }
  
              var childList = cache ? cache.entryForKey(this.tileKey) : null;
              if (!childList) {
                  childList = this.subdivide(level, tileFactory);
                  if (childList && cache) {
                      cache.putEntry(this.tileKey, childList, 4 * childList[0].size());
                  }
              }
  
              return childList;
          };
  
          /**
           * Indicates whether this tile should be subdivided based on the current navigation state and a specified
           * detail factor.
           * @param {DrawContext} dc The current draw context.
           * @param {Number} detailFactor The detail factor to consider.
           * @returns {boolean} true If the tile should be subdivided, otherwise false.
           */
          Tile.prototype.mustSubdivide = function (dc, detailFactor) {
              // Split when the cell height (length of a texel) becomes greater than the specified fraction of the eye
              // distance. The fraction is specified as a power of 10. For example, a detail factor of 3 means split when
              // the cell height becomes more than one thousandth of the eye distance. Another way to say it is, use the
              // current tile if the cell height is less than the specified fraction of the eye distance.
              //
              // Note: It's tempting to instead compare a screen pixel size to the texel size, but that calculation is
              // window-size dependent and results in selecting an excessive number of tiles when the window is large.
  
              var cellSize = dc.globe.equatorialRadius * this.texelSize,
                  distance = this.distanceTo(dc.navigatorState.eyePoint),
                  pixelSize = dc.navigatorState.pixelSizeAtDistance(distance);
  
              return cellSize > Math.max(detailFactor * pixelSize, 0.5);
          };
  
          /**
           * Updates this tile's frame-dependent properties as necessary, according to the specified draw context.
           * <p>
           * The tile's frame-dependent properties, include the extent (bounding volume). These properties are dependent
           * on the tile's sector and the elevation values currently in memory, and change when those dependencies change.
           * Therefore <code>update</code> must be called once per frame before the extent and any other frame-dependent
           * properties are used. <code>update</code> intelligently determines when it is necessary to recompute these
           * properties, and does nothing if the state of all dependencies has not changed since the last call.
           * @param {DrawContext} dc The current draw context.
           */
          Tile.prototype.update = function (dc) {
              var elevationTimestamp = dc.globe.elevationTimestamp(),
                  verticalExaggeration = dc.verticalExaggeration,
                  globeStateKey = dc.globeStateKey;
  
              if (this.updateTimestamp != elevationTimestamp
                  || this.updateVerticalExaggeration != verticalExaggeration
                  || this.updateGlobeStateKey != globeStateKey) {
  
                  this.doUpdate(dc);
                  dc.frameStatistics.incrementTileUpdateCount(1);
  
                  // Set the geometry extent to the globe's elevation timestamp on which the geometry is based. This
                  // ensures that the geometry timestamp can be reliably compared to the elevation timestamp in subsequent
                  // frames.
                  this.updateTimestamp = elevationTimestamp;
                  this.updateVerticalExaggeration = verticalExaggeration;
                  this.updateGlobeStateKey = globeStateKey;
              }
          };
  
          /**
           * Updates this tile's frame-dependent properties according to the specified draw context.
           * @param {DrawContext} dc The current draw context.
           * @protected
           */
          Tile.prototype.doUpdate = function (dc) {
              // Compute the minimum and maximum world coordinate height for this tile's sector by multiplying the minimum
              // and maximum elevations by the scene's vertical exaggeration. This ensures that the elevations to used
              // build the terrain are contained by this tile's extent. Use zero if the globe as no elevations in this
              // tile's sector.
              var globe = dc.globe,
                  verticalExaggeration = dc.verticalExaggeration,
                  extremes = globe.minAndMaxElevationsForSector(this.sector),
                  minHeight = extremes ? (extremes[0] * verticalExaggeration) : 0,
                  maxHeight = extremes ? (extremes[1] * verticalExaggeration) : 0;
              if (minHeight == maxHeight) {
                  minHeight = maxHeight + 10; // TODO: Determine if this is necessary.
              }
  
              // Compute a bounding box for this tile that contains the terrain surface in the tile's coverage area.
              if (!this.extent) {
                  this.extent = new BoundingBox();
              }
              this.extent.setToSector(this.sector, globe, minHeight, maxHeight);
  
              // Compute the cartesian points for a 3x3 geographic grid. This grid captures sufficiently close sample
              // points in order to estimate the distance from the viewer to this tile.
              if (!this.samplePoints) {
                  this.sampleElevations = new Float64Array(9);
                  this.samplePoints = new Float64Array(3 * this.sampleElevations.length);
              }
              WWUtil.fillArray(this.sampleElevations, 0.5 * (minHeight + maxHeight));
              globe.computePointsForGrid(this.sector, 3, 3, this.sampleElevations, Vec3.ZERO, this.samplePoints);
  
              // Compute the reference point used as a local coordinate origin for the tile.
              if (!this.referencePoint) {
                  this.referencePoint = new Vec3(0, 0, 0);
              }
  
              globe.computePointFromPosition(this.sector.centroidLatitude(), this.sector.centroidLongitude(), 0,
                  this.referencePoint);
          };
  
          /**
           * Computes a row number for a tile within a level given the tile's latitude.
           * @param {Number} delta The level's latitudinal tile delta in degrees.
           * @param {Number} latitude The tile's minimum latitude.
           * @returns {Number} The computed row number.
           */
          Tile.computeRow = function (delta, latitude) {
              var row = Math.floor((latitude + 90) / delta);
  
              // If latitude is at the end of the grid, subtract 1 from the computed row to return the last row.
              if (latitude == 90) {
                  row -= 1;
              }
  
              return row;
          };
  
          /**
           * Computes a column number for a tile within a level given the tile's longitude.
           * @param {Number} delta The level's longitudinal tile delta in degrees.
           * @param {Number} longitude The tile's minimum longitude.
           * @returns {Number} The computed column number.
           */
          Tile.computeColumn = function (delta, longitude) {
              var col = Math.floor((longitude + 180) / delta);
  
              // If longitude is at the end of the grid, subtract 1 from the computed column to return the last column.
              if (longitude == 180) {
                  col -= 1;
              }
  
              return col;
          };
  
          /**
           * Computes the last row number for a tile within a level given the tile's maximum latitude.
           * @param {Number} delta The level's latitudinal tile delta in degrees.
           * @param {Number} maxLatitude The tile's maximum latitude in degrees.
           * @returns {Number} The computed row number.
           */
          Tile.computeLastRow = function (delta, maxLatitude) {
              var row = Math.ceil((maxLatitude + 90) / delta - 1);
  
              // If max latitude is in the first row, set the max row to 0.
              if (maxLatitude + 90 < delta) {
                  row = 0;
              }
  
              return row;
          };
  
          /**
           * Computes the last column number for a tile within a level given the tile's maximum longitude.
           * @param {Number} delta The level's longitudinal tile delta in degrees.
           * @param {Number} maxLongitude The tile's maximum longitude in degrees.
           * @returns {Number} The computed column number.
           */
          Tile.computeLastColumn = function (delta, maxLongitude) {
              var col = Math.ceil((maxLongitude + 180) / delta - 1);
  
              // If max longitude is in the first column, set the max column to 0.
              if (maxLongitude + 180 < delta) {
                  col = 0;
              }
  
              return col;
          };
  
          /**
           * Computes a sector spanned by a tile with the specified level number, row and column.
           * @param {Level} level The tile's level number.
           * @param {Number} row The tile's row number.
           * @param {Number} column The tile's column number.
           * @returns {Sector} The sector spanned by the tile.
           * @throws {ArgumentError} If the specified level is null or undefined or the row or column are less than zero.
           */
          Tile.computeSector = function (level, row, column) {
              if (!level) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector", "missingLevel"));
              }
  
              if (row < 0 || column < 0) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector",
                          "The specified row or column is less than zero."));
              }
  
              var deltaLat = level.tileDelta.latitude,
                  deltaLon = level.tileDelta.longitude,
  
                  minLat = -90 + row * deltaLat,
                  minLon = -180 + column * deltaLon,
                  maxLat = minLat + deltaLat,
                  maxLon = minLon + deltaLon;
  
              return new Sector(minLat, maxLat, minLon, maxLon);
          };
  
          /**
           * Creates all tiles for a specified level number.
           * @param {Level} level The level to create the tiles for.
           * @param {TileFactory} tileFactory The tile factory to use for creating tiles.
           * @param {Tile[]} result An array in which to return the results.
           * @throws {ArgumentError} If any argument is null or undefined.
           */
          Tile.createTilesForLevel = function (level, tileFactory, result) {
              if (!level) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingLevel"));
              }
  
              if (!tileFactory) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel",
                          "The specified tile factory is null or undefined"));
              }
  
              if (!result) {
                  throw new ArgumentError(
                      Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingResult"));
              }
  
              var deltaLat = level.tileDelta.latitude,
                  deltaLon = level.tileDelta.longitude,
  
                  sector = level.sector,
                  firstRow = Tile.computeRow(deltaLat, sector.minLatitude),
                  lastRow = Tile.computeRow(deltaLat, sector.maxLatitude),
  
                  firstCol = Tile.computeColumn(deltaLon, sector.minLongitude),
                  lastCol = Tile.computeColumn(deltaLon, sector.maxLongitude),
  
                  firstRowLat = -90 + firstRow * deltaLat,
                  firstRowLon = -180 + firstCol * deltaLon,
  
                  minLat = firstRowLat,
                  minLon,
                  maxLat,
                  maxLon;
  
              for (var row = firstRow; row <= lastRow; row += 1) {
                  maxLat = minLat + deltaLat;
                  minLon = firstRowLon;
  
                  for (var col = firstCol; col <= lastCol; col += 1) {
                      maxLon = minLon + deltaLon;
                      var tileSector = new Sector(minLat, maxLat, minLon, maxLon),
                          tile = tileFactory.createTile(tileSector, level, row, col);
                      result.push(tile);
  
                      minLon = maxLon;
                  }
  
                  minLat = maxLat;
              }
          };
  
          return Tile;
      });