earthengine-api/javascript/src/customfunction.js
2020-09-09 13:51:16 -07:00

238 lines
7.0 KiB
JavaScript

/**
* @fileoverview An object representing a custom EE Function.
*/
goog.provide('ee.CustomFunction');
goog.require('ee.ComputedObject');
goog.require('ee.Function');
goog.require('ee.Number');
goog.require('ee.Serializer');
goog.require('ee.String');
goog.require('ee.Types');
goog.require('ee.rpc_node');
goog.require('goog.array');
/**
* Creates a function defined by a given expression with unbound variables.
* The expression is created by evaluating the given JavaScript function
* using variables as placeholders.
*
* @param {ee.Function.Signature} signature The function's signature. If any of
* the argument names are null, their names will be generated
* deterministically, based on the body.
* @param {Function} body The JavaScript function to evaluate.
*
* @constructor
* @extends {ee.Function}
*/
ee.CustomFunction = function(signature, body) {
if (!(this instanceof ee.CustomFunction)) {
return ee.ComputedObject.construct(ee.CustomFunction, arguments);
}
var vars = [];
var args = signature['args'];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var type = ee.Types.nameToClass(arg['type']);
vars.push(ee.CustomFunction.variable(type, arg['name']));
}
// Check that the method returns something, before we try
// encoding it in resolveNamelessArgs_().
if (body.apply(null, vars) === undefined) {
throw Error('User-defined methods must return a value.');
}
/**
* The signature of the function.
* @type {ee.Function.Signature}
* @private
*/
this.signature_ = ee.CustomFunction.resolveNamelessArgs_(
signature, vars, body);
/**
* The function evaluated using placeholders.
* @type {*}
* @private
*/
this.body_ = body.apply(null, vars);
};
goog.inherits(ee.CustomFunction, ee.Function);
// Exporting manually to avoid marking the class public in the docs.
goog.exportSymbol('ee.CustomFunction', ee.CustomFunction);
/** @override */
ee.CustomFunction.prototype.encode = function(encoder) {
return {
'type': 'Function',
'argumentNames': goog.array.map(
this.signature_['args'], function(arg) { return arg['name']; }),
'body': encoder(this.body_)
};
};
/** @override */
ee.CustomFunction.prototype.encodeCloudValue = function(encoder) {
return ee.rpc_node.functionDefinition(
this.signature_['args'].map(arg => arg['name']), encoder(this.body_));
};
/** @override */
ee.CustomFunction.prototype.encodeCloudInvocation = function(encoder, args) {
return ee.rpc_node.functionByReference(encoder(this), args);
};
/** @override */
ee.CustomFunction.prototype.getSignature = function() {
return this.signature_;
};
/**
* Returns a placeholder variable with a given name that implements a given
* EE type.
*
* @param {Function} type A type to mimic.
* @param {string?} name The name of the variable as it will appear in the
* arguments of the custom functions that use this variable. If null, a
* name will be auto-generated in resolveNamelessArgs_().
* @return {*} A variable with the given name implementing the given type.
*/
ee.CustomFunction.variable = function(type, name) {
type = type || Object;
if (!(type.prototype instanceof ee.ComputedObject)) {
// Try co convert to an EE type.
if (!type || type == Object) {
type = ee.ComputedObject;
} else if (type == String) {
type = ee.String;
} else if (type == Number) {
type = ee.Number;
} else if (type == Array) {
type = goog.global['ee']['List'];
} else {
throw Error('Variables must be of an EE type, ' +
'e.g. ee.Image or ee.Number.');
}
}
/**
* Avoid Object.create() for backwards compatibility.
* @constructor
*/
var klass = function(name) {
this.func = null;
this.args = null;
this.varName = name;
};
klass.prototype = type.prototype;
return new klass(name);
};
/**
* Creates a CustomFunction calling a given native function with the specified
* return type and argument types and auto-generated argument names.
*
* @param {Function} func The native function to wrap.
* @param {string|Function} returnType The type of the return value, either
* as a string or a constructor/class reference.
* @param {Array.<string|Function>} arg_types The types of the arguments,
* either as strings or constructor/class references.
* @return {!ee.CustomFunction} The constructed CustomFunction.
*/
ee.CustomFunction.create = function(func, returnType, arg_types) {
var stringifyType = function(type) {
if (typeof type === 'string') {
return type;
} else {
return ee.Types.classToName(type);
}
};
var args = goog.array.map(arg_types, function(argType) {
return {
'name': null,
'type': stringifyType(argType)
};
});
var signature = {
'name': '',
'returns': stringifyType(returnType),
'args': args
};
return new ee.CustomFunction(signature, func);
};
/**
* Deterministically generates names for the unnamed variables, based on the
* body.
*
* @param {!ee.Function.Signature} signature The signature which may contain
* null argument names.
* @param {!Array<!ee.ComputedObject>} vars A list of variables, some of which
* may be nameless. These will be updated to include names when this
* method returns.
* @param {!Function} body The JavaScript function to evaluate.
* @return {!ee.Function.Signature} The signature with null arg names resolved.
* @private
* @suppress {accessControls} We are accessing the protected varName.
*/
ee.CustomFunction.resolveNamelessArgs_ = function(signature, vars, body) {
const namelessArgIndices = [];
for (let i = 0; i < vars.length; i++) {
if (vars[i].varName === null) {
namelessArgIndices.push(i);
}
}
// Do we have any nameless arguments at all?
if (namelessArgIndices.length === 0) {
return signature;
}
// Generate the name base by counting the number of custom functions
// within the body.
const countFunctions = (expression) => {
const countNodes = (nodes) =>
nodes.map(countNode).reduce((a, b) => a + b, 0);
const countNode = (node) => {
if (node.functionDefinitionValue) {
return 1;
} else if (node.arrayValue) {
return countNodes(node.arrayValue.values);
} else if (node.dictionaryValue) {
return countNodes(Object.values(node.dictionaryValue.values));
} else if (node.functionInvocationValue) {
const fn = node.functionInvocationValue;
return countNodes(Object.values(fn.arguments));
}
return 0;
};
return countNodes(Object.values(expression.values));
};
const serializedBody =
ee.Serializer.encodeCloudApiExpression(body.apply(null, vars));
const baseName = `_MAPPING_VAR_${countFunctions(serializedBody)}_`;
// Update the vars and signature by the name.
for (let i = 0; i < namelessArgIndices.length; i++) {
const index = namelessArgIndices[i];
const name = baseName + i;
vars[index].varName = name;
signature['args'][index]['name'] = name;
}
return signature;
};