mirror of
https://github.com/google/earthengine-api.git
synced 2025-12-08 19:26:12 +00:00
659 lines
19 KiB
JavaScript
659 lines
19 KiB
JavaScript
/**
|
|
* @fileoverview Initialization and convenience functions for the EE library.
|
|
*/
|
|
|
|
goog.provide('ee');
|
|
goog.provide('ee.Algorithms');
|
|
goog.provide('ee.InitState');
|
|
|
|
goog.require('ee.ApiFunction');
|
|
goog.require('ee.Collection');
|
|
goog.require('ee.ComputedObject');
|
|
goog.require('ee.CustomFunction');
|
|
goog.require('ee.Date');
|
|
goog.require('ee.Dictionary');
|
|
goog.require('ee.Element');
|
|
goog.require('ee.Encodable');
|
|
goog.require('ee.Feature');
|
|
goog.require('ee.FeatureCollection');
|
|
goog.require('ee.Filter');
|
|
goog.require('ee.Function');
|
|
goog.require('ee.Geometry');
|
|
goog.require('ee.Image');
|
|
goog.require('ee.ImageCollection');
|
|
goog.require('ee.List');
|
|
goog.require('ee.Number');
|
|
goog.require('ee.String');
|
|
goog.require('ee.Terrain');
|
|
goog.require('ee.Types');
|
|
goog.require('ee.data');
|
|
goog.require('goog.array');
|
|
goog.require('goog.object');
|
|
|
|
|
|
/**
|
|
* Initialize the library. If this hasn't been called by the time any
|
|
* object constructor is used, it will be called then. If this is called
|
|
* a second time with a different baseurl or tileurl, this doesn't do an
|
|
* un-initialization of e.g.: the previously loaded Algorithms, but will
|
|
* overwrite them and let point at alternate servers.
|
|
*
|
|
* If initialize() is first called in asynchronous mode (by passing a success
|
|
* callback), any future asynchronous mode calls will add their callbacks to a
|
|
* queue and all the callbacks will be run together.
|
|
*
|
|
* If a synchronous mode call is made after any number of asynchronous calls,
|
|
* it will block and execute all the previously supplied callbacks before
|
|
* returning.
|
|
*
|
|
* @param {string?=} opt_baseurl The (proxied) EarthEngine REST API endpoint.
|
|
* @param {string?=} opt_tileurl The (unproxied) EarthEngine REST tile endpoint.
|
|
* @param {function()?=} opt_successCallback An optional callback to be invoked
|
|
* when the initialization is successful. If not provided, the
|
|
* initialization is done synchronously.
|
|
* @param {function(Error)?=} opt_errorCallback An optional callback to be
|
|
* invoked with an error if the initialization fails.
|
|
* @param {string?=} opt_xsrfToken A string to pass in the "xsrfToken"
|
|
* parameter of EE API XHRs.
|
|
* @export
|
|
*/
|
|
ee.initialize = function(
|
|
opt_baseurl, opt_tileurl, opt_successCallback, opt_errorCallback,
|
|
opt_xsrfToken) {
|
|
// If we're already initialized and not getting new parameters, just return.
|
|
if (ee.ready_ == ee.InitState.READY && !opt_baseurl && !opt_tileurl) {
|
|
if (opt_successCallback) {
|
|
opt_successCallback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var isAsynchronous = goog.isDefAndNotNull(opt_successCallback);
|
|
|
|
// Register the error callback.
|
|
if (opt_errorCallback) {
|
|
if (isAsynchronous) {
|
|
ee.errorCallbacks_.push(opt_errorCallback);
|
|
} else {
|
|
throw Error('Can\'t pass an error callback without a success callback.');
|
|
}
|
|
}
|
|
|
|
// If we're already loading, and this is asynchronous, register the success
|
|
// callback and return. Synchronous initialization runs immediately,
|
|
// effectively overriding the currently running asynchronous one.
|
|
if (ee.ready_ == ee.InitState.LOADING && isAsynchronous) {
|
|
ee.successCallbacks_.push(opt_successCallback);
|
|
return;
|
|
}
|
|
|
|
ee.ready_ = ee.InitState.LOADING;
|
|
ee.data.initialize(opt_baseurl, opt_tileurl, opt_xsrfToken);
|
|
|
|
if (isAsynchronous) {
|
|
ee.successCallbacks_.push(opt_successCallback);
|
|
ee.ApiFunction.initialize(
|
|
ee.initializationSuccess_, ee.initializationFailure_);
|
|
} else {
|
|
try {
|
|
ee.ApiFunction.initialize();
|
|
ee.initializationSuccess_();
|
|
} catch (e) {
|
|
ee.initializationFailure_(e);
|
|
throw e;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Reset the library to its base state. Useful for re-initializing to a
|
|
* different server.
|
|
* @export
|
|
*/
|
|
ee.reset = function() {
|
|
ee.ready_ = ee.InitState.NOT_READY;
|
|
ee.data.reset();
|
|
ee.ApiFunction.reset();
|
|
ee.Date.reset();
|
|
ee.Dictionary.reset();
|
|
ee.Element.reset();
|
|
ee.Image.reset();
|
|
ee.Feature.reset();
|
|
ee.Collection.reset();
|
|
ee.ImageCollection.reset();
|
|
ee.FeatureCollection.reset();
|
|
ee.Filter.reset();
|
|
ee.Geometry.reset();
|
|
ee.List.reset();
|
|
ee.Number.reset();
|
|
ee.String.reset();
|
|
ee.Terrain.reset();
|
|
ee.resetGeneratedClasses_();
|
|
// Can't simply reassign ee.Algorithms to {} since it's been exported by
|
|
// reference.
|
|
goog.object.clear(ee.Algorithms);
|
|
};
|
|
|
|
|
|
/**
|
|
* The possible states for the library initialization function. We want
|
|
* to prohibit multiple overlapping calls, and allow the user a way to poll
|
|
* to see what the state is.
|
|
*
|
|
* @enum {string}
|
|
* @export
|
|
*/
|
|
ee.InitState = {
|
|
NOT_READY: 'not_ready',
|
|
LOADING: 'loading',
|
|
READY: 'ready'
|
|
};
|
|
// @export does not work on enum properties.
|
|
goog.exportSymbol('ee.InitState.NOT_READY', ee.InitState.NOT_READY);
|
|
goog.exportSymbol('ee.InitState.LOADING', ee.InitState.LOADING);
|
|
goog.exportSymbol('ee.InitState.READY', ee.InitState.READY);
|
|
|
|
|
|
/**
|
|
* A flag to indicate the initialization state.
|
|
* @type {ee.InitState}
|
|
* @private
|
|
*/
|
|
ee.ready_ = ee.InitState.NOT_READY;
|
|
|
|
|
|
/**
|
|
* The list of callbacks to call on successful initialization. Added by
|
|
* initialize() and cleared by initializationSuccess_() and
|
|
* initializationFailure_().
|
|
* @type {Array.<function()>}
|
|
* @private
|
|
*/
|
|
ee.successCallbacks_ = [];
|
|
|
|
|
|
/**
|
|
* The list of callbacks to call on failed initialization. Added by
|
|
* initialize() and cleared by initializationSuccess_() and
|
|
* initializationFailure_().
|
|
* @type {Array.<function(Error)>}
|
|
* @private
|
|
*/
|
|
ee.errorCallbacks_ = [];
|
|
|
|
|
|
/**
|
|
* @type {number} The size of a tile generated by the /map servlet.
|
|
* @const
|
|
* @export
|
|
*/
|
|
ee.TILE_SIZE = 256;
|
|
|
|
|
|
/**
|
|
* The list of auto-generated class names.
|
|
* @type {Array.<string>}
|
|
* @private
|
|
*/
|
|
ee.generatedClasses_ = [];
|
|
|
|
|
|
/**
|
|
* A dictionary of algorithms that are not bound to a specific class. Can
|
|
* contain nested namespaces (e.g. ee.Algorithms.Landsat.SimpleComposite).
|
|
* @type {Object.<Object|Function>}
|
|
* @export
|
|
*/
|
|
ee.Algorithms = {};
|
|
|
|
|
|
/**
|
|
* @return {ee.InitState} The initialization status.
|
|
*/
|
|
ee.ready = function() {
|
|
return ee.ready_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Call a function with the given positional arguments.
|
|
*
|
|
* @param {ee.Function|string} func The function to call. Either an
|
|
* ee.Function object or the name of an API function.
|
|
* @param {...*} var_args Positional arguments to pass to the function.
|
|
* @return {ee.ComputedObject} An object representing the called function.
|
|
* If the signature specifies a recognized return type, the returned
|
|
* value will be cast to that type.
|
|
* @export
|
|
*/
|
|
ee.call = function(func, var_args) {
|
|
if (goog.isString(func)) {
|
|
func = new ee.ApiFunction(func);
|
|
}
|
|
// Extract var_args.
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
// Call func.call with the extracted agrs.
|
|
return ee.Function.prototype.call.apply(func, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Call a function with a dictionary of named arguments.
|
|
*
|
|
* @param {ee.Function|string} func The function to call. Either an
|
|
* ee.Function object or the name of an API function.
|
|
* @param {Object} namedArgs A dictionary of arguments to the function.
|
|
* @return {ee.ComputedObject} An object representing the called function.
|
|
* If the signature specifies a recognized return type, the returned
|
|
* value will be cast to that type.
|
|
* @export
|
|
*/
|
|
ee.apply = function(func, namedArgs) {
|
|
if (goog.isString(func)) {
|
|
func = new ee.ApiFunction(func);
|
|
}
|
|
return func.apply(namedArgs);
|
|
};
|
|
|
|
|
|
/**
|
|
* Finishes the initialization of the library, assuming ApiFunction has been
|
|
* initialized successfully.
|
|
* @private
|
|
*/
|
|
ee.initializationSuccess_ = function() {
|
|
if (ee.ready_ != ee.InitState.LOADING) {
|
|
// We have already been called. Can happen if a blocking initialization is
|
|
// started while an asynchronous one is in progress. The asynchronous one
|
|
// will report success after the synchronous one has already reported
|
|
// success or failure.
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Update classes with bound methods.
|
|
ee.Date.initialize();
|
|
ee.Dictionary.initialize();
|
|
ee.Element.initialize();
|
|
ee.Image.initialize();
|
|
ee.Feature.initialize();
|
|
ee.Collection.initialize();
|
|
ee.ImageCollection.initialize();
|
|
ee.FeatureCollection.initialize();
|
|
ee.Filter.initialize();
|
|
ee.Geometry.initialize();
|
|
ee.List.initialize();
|
|
ee.Number.initialize();
|
|
ee.String.initialize();
|
|
ee.Terrain.initialize();
|
|
|
|
// Generate trivial classes.
|
|
ee.initializeGeneratedClasses_();
|
|
ee.initializeUnboundMethods_();
|
|
} catch (e) {
|
|
ee.initializationFailure_(e);
|
|
return;
|
|
}
|
|
|
|
// Declare ourselves ready.
|
|
ee.ready_ = ee.InitState.READY;
|
|
|
|
// Clear failure callbacks.
|
|
ee.errorCallbacks_ = [];
|
|
|
|
// Call success callbacks.
|
|
while (ee.successCallbacks_.length > 0) {
|
|
// If one of these throws an exception, we explode. Maybe we should ignore
|
|
// it and continue?
|
|
ee.successCallbacks_.shift()();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Reports initialization failure.
|
|
* @param {Error} e The cause of the failure.
|
|
* @private
|
|
*/
|
|
ee.initializationFailure_ = function(e) {
|
|
if (ee.ready_ != ee.InitState.LOADING) {
|
|
// Duplicate call. See reasoning in ee.initializationSuccess_.
|
|
return;
|
|
}
|
|
|
|
// Declare ourselves unready.
|
|
ee.ready_ = ee.InitState.NOT_READY;
|
|
|
|
// Clear success callbacks.
|
|
ee.successCallbacks_ = [];
|
|
|
|
// Call failure callbacks.
|
|
while (ee.errorCallbacks_.length > 0) {
|
|
// If one of these throws an exception, we explode. Maybe we should ignore
|
|
// it and continue?
|
|
ee.errorCallbacks_.shift()(e);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Wrap an argument in an object of the specified class. This is used to
|
|
* e.g.: promote numbers or strings to Images and arrays to Collections.
|
|
*
|
|
* @param {?} arg The object to promote.
|
|
* @param {string} klass The expected type.
|
|
* @return {?} The argument promoted if the class is recognized, otherwise the
|
|
* original argument.
|
|
* @private
|
|
* @suppress {accessControls} We are calling functions with partial promotion.
|
|
*/
|
|
ee.promote_ = function(arg, klass) {
|
|
if (goog.isNull(arg)) {
|
|
return null;
|
|
} else if (!goog.isDef(arg)) {
|
|
return undefined;
|
|
}
|
|
|
|
var exportedEE = goog.global['ee'];
|
|
|
|
switch (klass) {
|
|
case 'Image':
|
|
return new ee.Image(/** @type {Object} */ (arg));
|
|
case 'Feature':
|
|
if (arg instanceof ee.Collection) {
|
|
// TODO(user): Decide whether we want to leave this in. It can be
|
|
// quite dangerous on large collections.
|
|
return ee.ApiFunction._call(
|
|
'Feature', ee.ApiFunction._call('Collection.geometry', arg));
|
|
} else {
|
|
return new ee.Feature(/** @type {Object} */ (arg));
|
|
}
|
|
case 'Element':
|
|
if (arg instanceof ee.Element) {
|
|
// Already an Element.
|
|
return arg;
|
|
} else if (arg instanceof ee.Geometry) {
|
|
// Geometries get promoted to Features.
|
|
return new ee.Feature(/** @type {ee.Geometry} */ (arg));
|
|
} else if (arg instanceof ee.ComputedObject) {
|
|
// Try a cast.
|
|
var co = /** @type {ee.ComputedObject} */ (arg);
|
|
return new ee.Element(co.func, co.args, co.varName);
|
|
} else {
|
|
// No way to convert.
|
|
throw Error('Cannot convert ' + arg + ' to Element.');
|
|
}
|
|
case 'Geometry':
|
|
if (arg instanceof ee.FeatureCollection) {
|
|
return ee.ApiFunction._call('Collection.geometry', arg);
|
|
} else {
|
|
return new ee.Geometry(/** @type {?} */ (arg));
|
|
}
|
|
case 'FeatureCollection':
|
|
case 'Collection': // For now this is synonymous with FeatureCollection.
|
|
if (arg instanceof ee.Collection) {
|
|
return arg;
|
|
} else {
|
|
return new ee.FeatureCollection(/** @type {?} */ (arg));
|
|
}
|
|
case 'ImageCollection':
|
|
return new ee.ImageCollection(/** @type {?} */ (arg));
|
|
case 'Filter':
|
|
return new ee.Filter(/** @type {Object} */ (arg));
|
|
case 'Algorithm':
|
|
if (goog.isString(arg)) {
|
|
// An API function name.
|
|
return new ee.ApiFunction(arg);
|
|
} else if (goog.isFunction(arg)) {
|
|
// A native function that needs to be wrapped.
|
|
return ee.CustomFunction.create(
|
|
arg, 'Object', goog.array.repeat('Object', arg.length));
|
|
} else if (arg instanceof ee.Encodable) {
|
|
// An ee.Function or a computed function like the return value of
|
|
// Image.parseExpression().
|
|
return arg;
|
|
} else {
|
|
throw Error('Argument is not a function: ' + arg);
|
|
}
|
|
case 'String':
|
|
if (ee.Types.isString(arg) ||
|
|
arg instanceof ee.String ||
|
|
arg instanceof ee.ComputedObject) {
|
|
return new ee.String(arg);
|
|
} else {
|
|
return arg;
|
|
}
|
|
case 'Dictionary':
|
|
if (ee.Types.isRegularObject(arg)) {
|
|
return arg;
|
|
} else {
|
|
return new ee.Dictionary(/** @type {?} */ (arg));
|
|
}
|
|
case 'List':
|
|
return new ee.List(/** @type {?} */ (arg));
|
|
case 'Number':
|
|
case 'Float':
|
|
case 'Long':
|
|
case 'Integer':
|
|
case 'Short':
|
|
case 'Byte':
|
|
return new ee.Number(/** @type {?} */ (arg));
|
|
default:
|
|
// Handle dynamically generated classes.
|
|
if (klass in exportedEE) {
|
|
var ctor = ee.ApiFunction.lookupInternal(klass);
|
|
if (arg instanceof exportedEE[klass]) {
|
|
// Return unchanged.
|
|
return arg;
|
|
} else if (ctor) {
|
|
// The client-side constructor will call the server-side constructor.
|
|
return new exportedEE[klass](arg);
|
|
} else if (goog.isString(arg)) {
|
|
if (arg in exportedEE[klass]) {
|
|
// arg is the name of a method on klass.
|
|
return exportedEE[klass][arg].call();
|
|
} else {
|
|
throw new Error('Unknown algorithm: ' + klass + '.' + arg);
|
|
}
|
|
} else {
|
|
// Client-side cast.
|
|
return new exportedEE[klass](arg);
|
|
}
|
|
} else {
|
|
// Don't know.
|
|
return arg;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Puts any unbound API methods on ee.Algorithms.
|
|
*
|
|
* @private
|
|
*/
|
|
ee.initializeUnboundMethods_ = function() {
|
|
var unbound = ee.ApiFunction.unboundFunctions();
|
|
goog.object.getKeys(unbound).sort().forEach(function(name) {
|
|
var func = unbound[name];
|
|
var signature = func.getSignature();
|
|
if (signature['hidden']) {
|
|
return;
|
|
}
|
|
|
|
// Create nested objects as needed.
|
|
var nameParts = name.split('.');
|
|
var target = ee.Algorithms;
|
|
target['signature'] = {};
|
|
while (nameParts.length > 1) {
|
|
var first = nameParts[0];
|
|
if (!(first in target)) {
|
|
// We must add a signature property so the playground docbox recognizes
|
|
// these objects as parts of the API.
|
|
target[first] = {'signature': {}};
|
|
}
|
|
target = target[first];
|
|
nameParts = goog.array.slice(nameParts, 1);
|
|
}
|
|
|
|
// Attach the function.
|
|
var bound = function(var_args) {
|
|
return func.callOrApply(
|
|
undefined, Array.prototype.slice.call(arguments, 0));
|
|
};
|
|
bound['signature'] = signature;
|
|
bound.toString = goog.bind(func.toString, func);
|
|
target[nameParts[0]] = bound;
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Autogenerate any classes that meet the following criteria:
|
|
* - There's 1 or more functions named TYPE.*
|
|
* - There's 1 or more functions that return that type.
|
|
* - The class doesn't already exist as an ee.TYPE.
|
|
*
|
|
* @private
|
|
* @suppress {accessControls} We update ApiFunction.boundSignatures_.
|
|
*/
|
|
ee.initializeGeneratedClasses_ = function() {
|
|
var signatures = ee.ApiFunction.allSignatures();
|
|
|
|
// Collect all the type names from functions, and all the return types.
|
|
var names = {};
|
|
var returnTypes = {};
|
|
for (var sig in signatures) {
|
|
var type;
|
|
if (sig.indexOf('.') != -1) {
|
|
type = sig.slice(0, sig.indexOf('.'));
|
|
} else {
|
|
type = sig;
|
|
}
|
|
names[type] = true;
|
|
// Strip off extra type info. e.g.: Dictionary<Object>
|
|
var rtype = signatures[sig]['returns'].replace(/<.*>/, '');
|
|
returnTypes[rtype] = true;
|
|
}
|
|
|
|
// Create classes with names in both, excluding any types that already exist.
|
|
var exportedEE = goog.global['ee'];
|
|
for (var name in names) {
|
|
if (name in returnTypes && !(name in exportedEE)) {
|
|
exportedEE[name] = ee.makeClass_(name);
|
|
ee.generatedClasses_.push(name);
|
|
// Mark this class as a constructor if it is.
|
|
if (signatures[name]) {
|
|
exportedEE[name]['signature'] = signatures[name];
|
|
exportedEE[name]['signature']['isConstructor'] = true;
|
|
ee.ApiFunction.boundSignatures_[name] = true;
|
|
} else {
|
|
exportedEE[name]['signature'] = {};
|
|
}
|
|
}
|
|
}
|
|
ee.Types.registerClasses(exportedEE);
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove the classes added by initializeGeneratedClasses.
|
|
* @private
|
|
*/
|
|
ee.resetGeneratedClasses_ = function() {
|
|
var exportedEE = goog.global['ee'];
|
|
for (var i = 0; i < ee.generatedClasses_.length; i++) {
|
|
var name = ee.generatedClasses_[i];
|
|
ee.ApiFunction.clearApi(exportedEE[name]);
|
|
delete exportedEE[name];
|
|
}
|
|
ee.generatedClasses_ = [];
|
|
ee.Types.registerClasses(exportedEE);
|
|
};
|
|
|
|
|
|
/**
|
|
* Dynamically make an ee helper class.
|
|
*
|
|
* @param {string} name The name of the class to create.
|
|
* @return {Function} The generated class.
|
|
* @private
|
|
* @suppress {accessControls}
|
|
*/
|
|
ee.makeClass_ = function(name) {
|
|
/**
|
|
* Construct a new instance of the given class.
|
|
*
|
|
* @param {...*} var_args The constructor args. Can be one of:
|
|
* 1) A computed value to be promoted to this type.
|
|
* 2) Arguments to be passed to the algorithm with the same name as
|
|
* this class.
|
|
*
|
|
* @return {*} The newly created class.
|
|
*
|
|
* @constructor
|
|
* @extends {ee.ComputedObject}
|
|
*
|
|
* TODO(user): Generate docs for these classes.
|
|
*/
|
|
var target = function(var_args) {
|
|
var klass = goog.global['ee'][name];
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var onlyOneArg = (args.length == 1);
|
|
|
|
// Are we trying to cast something that's already of the right class?
|
|
if (onlyOneArg && args[0] instanceof klass) {
|
|
return args[0];
|
|
}
|
|
|
|
// Constructor safety.
|
|
if (!(this instanceof klass)) {
|
|
return ee.ComputedObject.construct(klass, args);
|
|
}
|
|
|
|
// Decide whether to call a server-side constructor or just do a
|
|
// client-side cast.
|
|
var ctor = ee.ApiFunction.lookupInternal(name);
|
|
var firstArgIsPrimitive = !(args[0] instanceof ee.ComputedObject);
|
|
var shouldUseConstructor = false;
|
|
if (ctor) {
|
|
if (!onlyOneArg) {
|
|
// Can't client-cast multiple arguments.
|
|
shouldUseConstructor = true;
|
|
} else if (firstArgIsPrimitive) {
|
|
// Can't cast a primitive.
|
|
shouldUseConstructor = true;
|
|
} else if (args[0].func != ctor) {
|
|
// We haven't already called the constructor on this object.
|
|
shouldUseConstructor = true;
|
|
}
|
|
}
|
|
|
|
// Apply our decision.
|
|
if (shouldUseConstructor) {
|
|
// Call manually to avoid having promote() called on the output.
|
|
goog.base(this, ctor, ctor.promoteArgs(ctor.nameArgs(args)));
|
|
} else {
|
|
// Just cast and hope for the best.
|
|
if (!onlyOneArg) {
|
|
// We don't know what to do with multiple args.
|
|
throw Error('Too many arguments for ee.' + name + '(): ' + args);
|
|
} else if (firstArgIsPrimitive) {
|
|
// Can't cast a primitive.
|
|
throw Error('Invalid argument for ee.' + name + '(): ' + args +
|
|
'. Must be a ComputedObject.');
|
|
}
|
|
var theOneArg = args[0];
|
|
goog.base(this, theOneArg.func, theOneArg.args, theOneArg.varName);
|
|
}
|
|
};
|
|
goog.inherits(target, ee.ComputedObject);
|
|
target.prototype.name = function() { return name; };
|
|
ee.ApiFunction.importApi(target, name, name);
|
|
return target;
|
|
};
|
|
|
|
// Set up type promotion rules as soon the library is loaded.
|
|
ee.Function.registerPromoter(ee.promote_);
|