earthengine-api/javascript/src/maplayeroverlay.js
Igor Nazarenko f89e198b16 v0.1.38
2014-09-25 14:58:39 -07:00

251 lines
7.7 KiB
JavaScript

goog.provide('ee.MapLayerOverlay');
goog.require('ee.MapTileManager');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.iter');
goog.require('goog.net.EventType');
goog.require('goog.structs.Set');
goog.require('goog.style');
/**
* A google.maps.MapType implementation used to display Earth Engine tiles.
* This class behaves much like an ImageMapType, but emits events when tiles
* are loaded.
* @param {string} url The url for fetching this layer's tiles.
* @param {string} mapId The map ID for fetching this layer's tiles.
* @param {string} token The temporary token for fetching tiles.
* @param {Object} init Initialization options, of the same form as a
* google.maps.ImageMapTypeOptions object.
* @constructor
* @extends {goog.events.EventTarget}
* @ignore
*/
ee.MapLayerOverlay = function(url, mapId, token, init) {
goog.base(this);
// Store mapId and token.
this.mapId = mapId;
this.token = token;
// Set ImageMapTypeOptions properties.
this.minZoom = init.minZoom || 0;
this.maxZoom = init.maxZoom || 20;
if (!window['google'] || !window['google']['maps']) {
throw Error("Google Maps API hasn't been initialized.");
}
this.tileSize = init.tileSize || new google.maps.Size(256, 256);
this.isPng = goog.isDef(init.isPng) ? init.isPng : true;
/**
* Array representing the set of tiles that are currently being
* loaded. They are added to this array by getTile() and removed
* with handleImageCompleted_().
* @private {!Array.<string>}
*/
this.tilesLoading_ = [];
/** @private {goog.structs.Set} The set of loaded tiles. */
this.tiles_ = new goog.structs.Set();
/** @private {goog.structs.Set} The set of failed tile IDs. */
this.tilesFailed_ = new goog.structs.Set();
/**
* Incrementing counter that helps make tile request ids unique.
* @private {number}
*/
this.tileCounter_ = 0;
/** @protected {string} The url from which to fetch tiles. */
this.url = url;
/** @private {number} The layer's opacity. */
this.opacity_ = 1.0;
/** @private {boolean} Whether the layer is currently visible. */
this.visible_ = true;
};
goog.inherits(ee.MapLayerOverlay, goog.events.EventTarget);
/** @enum {string} Event types. */
ee.MapLayerOverlay.EventType = {
TILE_LOADED: 'tileevent'
};
/**
* Send an event about a change in the number of outstanding tiles.
* @private
*/
ee.MapLayerOverlay.prototype.dispatchTileEvent_ = function() {
this.dispatchEvent(new ee.TileEvent_(this.tilesLoading_.length));
};
/**
* Implements getTile() for the google.maps.MapType interface.
* @param {google.maps.Point} coord Position of tile.
* @param {number} zoom Zoom level.
* @param {Node} ownerDocument Parent document.
* @return {Node} Element to be displayed as a map tile.
*/
ee.MapLayerOverlay.prototype.getTile = function(
coord, zoom, ownerDocument) {
var result;
var maxCoord = 1 << zoom;
if (zoom < this.minZoom || coord.y < 0 || coord.y >= maxCoord) {
// Construct and return the tile immediately.
var img = ownerDocument.createElement('IMG');
img.style.width = '0px';
img.style.height = '0px';
return img;
}
// Wrap longitude around.
var x = coord.x % maxCoord;
if (x < 0) {
x += maxCoord;
}
var tileId = [this.mapId, zoom, x, coord.y].join('/');
var src = [this.url, tileId].join('/') + '?token=' + this.token;
// Append a unique string to the tileid to make sure that
// repeated requests for the same tile and cancellations thereof
// (which may occur if the user quickly moves the map around)
// don't overwrite each other's state.
var uniqueTileId = tileId + '/' + this.tileCounter_;
this.tileCounter_ += 1;
var div = goog.dom.createDom('div', {'id': uniqueTileId});
// Use the current time in seconds as the priority for the tile
// loading queue. Smaller priorities move to the front of the queue,
// which is what we want - the Maps API loads the tiles in a spiral
// starting from the center, and we would like to preserve this order.
// Requests for tiles that are no longer visible won't clog the queue:
// if the map is moved around a lot, Maps API calls our releaseTile()
// method, and the obsolete requests will be removed from the queue.
var priority = new Date().getTime() / 1000;
this.tilesLoading_.push(uniqueTileId);
ee.MapTileManager.getInstance().send(
uniqueTileId, src, priority,
goog.bind(this.handleImageCompleted_, this, div, uniqueTileId));
this.dispatchTileEvent_();
result = div;
return result;
};
/** @return {number} The number of tiles successfully loaded. */
ee.MapLayerOverlay.prototype.getLoadedTilesCount = function() {
return this.tiles_.getCount();
};
/** @return {number} The number of tiles currently loading. */
ee.MapLayerOverlay.prototype.getLoadingTilesCount = function() {
return this.tilesLoading_.length;
};
/** @return {number} The number of tiles which have failed. */
ee.MapLayerOverlay.prototype.getFailedTilesCount = function() {
return this.tilesFailed_.getCount();
};
/**
* Implements releaseTile() for the google.maps.MapType
* interface.
* @param {Node} tileDiv The tile that has been released.
*/
ee.MapLayerOverlay.prototype.releaseTile = function(tileDiv) {
ee.MapTileManager.getInstance().abort(tileDiv.id);
var tileImg = goog.dom.getFirstElementChild(tileDiv);
this.tiles_.remove(tileImg);
this.tilesFailed_.remove(tileDiv.id);
};
/**
* Implements setOpacity() for the google.maps.MapType interface.
* @param {number} opacity The opacity to set this layer to.
*/
ee.MapLayerOverlay.prototype.setOpacity = function(opacity) {
this.opacity_ = opacity;
var iter = this.tiles_.__iterator__();
goog.iter.forEach(iter, function(tile) {
goog.style.setOpacity(tile, opacity);
});
};
// Export getTile and removeTile so they are not eliminated by dead
// code removal during compilation
goog.exportProperty(
ee.MapLayerOverlay.prototype,
'getTile',
ee.MapLayerOverlay.prototype.getTile);
goog.exportProperty(
ee.MapLayerOverlay.prototype,
'setOpacity',
ee.MapLayerOverlay.prototype.setOpacity);
goog.exportProperty(
ee.MapLayerOverlay.prototype,
'releaseTile',
ee.MapLayerOverlay.prototype.releaseTile);
/**
* Handle image 'load' and 'error' events. When the last one has
* finished, dispatch an ee.MapLayerOverlay.EventType.TILE_LOADED event.
* Handle bookkeeping to keep the tilesLoading_ array accurate.
* @param {Node} div Tile div to which images should be appended.
* @param {string} tileId The id of the tile that was requested.
* @param {goog.events.Event} e Image loading event.
* @private
*/
ee.MapLayerOverlay.prototype.handleImageCompleted_ = function(
div, tileId, e) {
if (e.type == goog.net.EventType.ERROR) {
// Forward error events.
goog.array.remove(this.tilesLoading_, tileId);
this.tilesFailed_.add(tileId);
this.dispatchEvent(e);
} else {
// Convert tile loading events to our own type.
goog.array.remove(this.tilesLoading_, tileId);
var tile;
if (e.target && (e.type == goog.events.EventType.LOAD)) {
tile = /** @type {Node} */ (e.target);
this.tiles_.add(tile);
if (this.opacity_ != 1.0) {
goog.style.setOpacity(/** @type {Element} */ (tile), this.opacity_);
}
div.appendChild(tile);
}
this.dispatchTileEvent_();
}
};
/**
* An event contaning the information about the tile status
* @param {number} count The number of outstanding tile requests.
* @constructor
* @private
* @extends {goog.events.Event}
*/
ee.TileEvent_ = function(count) {
goog.events.Event.call(this, ee.MapLayerOverlay.EventType.TILE_LOADED);
this.count = count;
};
goog.inherits(ee.TileEvent_, goog.events.Event);