Skip to content

Commit

Permalink
Handle unbound arguments from map and iterate calls on the JS client.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 681170281
  • Loading branch information
sufyanAbbasi authored and Google Earth Engine Authors committed Oct 3, 2024
1 parent f2e8070 commit 95b0277
Show file tree
Hide file tree
Showing 13 changed files with 45 additions and 25 deletions.
5 changes: 3 additions & 2 deletions javascript/src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ goog.requireType('ee.Geometry');
* @param {ee.Function} func The same argument as in ee.ComputedObject().
* @param {Object} args The same argument as in ee.ComputedObject().
* @param {string?=} opt_varName The same argument as in ee.ComputedObject().
* @param {boolean?=} opt_unbound The same argument as in ee.ComputedObject().
* @constructor
* @extends {ee.Element}
*/
ee.Collection = function(func, args, opt_varName) {
ee.Collection.base(this, 'constructor', func, args, opt_varName);
ee.Collection = function(func, args, opt_varName, opt_unbound) {
ee.Collection.base(this, 'constructor', func, args, opt_varName, opt_unbound);
ee.Collection.initialize();
};
goog.inherits(ee.Collection, ee.Element);
Expand Down
32 changes: 23 additions & 9 deletions javascript/src/computedobject.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ goog.requireType('ee.Function');
* and both 'func' and 'args' must be null. If all arguments are null, the
* object is considered an unnamed variable, and a name will be generated
* when it is included in an ee.CustomFunction.
* @param {?boolean=} opt_unbound Whether the object is unbound, i.e., called
* from a mapped or iterated function.
* @constructor
* @extends {ee.Encodable}
* @template T
*/
ee.ComputedObject = function(func, args, opt_varName) {
ee.ComputedObject = function(func, args, opt_varName, opt_unbound) {
// Constructor safety.
if (!(this instanceof ee.ComputedObject)) {
return ee.ComputedObject.construct(ee.ComputedObject, arguments);
Expand Down Expand Up @@ -76,6 +78,12 @@ ee.ComputedObject = function(func, args, opt_varName) {
* @protected
*/
this.varName = opt_varName || null;

/**
* Whether the computed object is an unbound variable.
* @type {boolean}
*/
this.unbound = !!opt_unbound;
};
goog.inherits(ee.ComputedObject, ee.Encodable);
// Exporting manually to avoid marking the class public in the docs.
Expand Down Expand Up @@ -153,14 +161,20 @@ ee.ComputedObject.prototype.encodeCloudValue = function(serializer) {
if (this.isVariable()) {
const name = this.varName || serializer.unboundName;
if (!name) {
// We are trying to call getInfo() or make some other server call inside a
// function passed to collection.map() or .iterate(), and the call uses
// one of the function arguments. The argument will be unbound outside of
// the map operation and cannot be evaluated. See the Count Functions case
// in customfunction.js for details on the unboundName mechanism.
// TODO(user): Report the name of the offending argument.
throw new Error(
'A mapped function\'s arguments cannot be used in client-side operations');
if (this.unbound) {
// We are trying to call getInfo() or make some other server call inside
// a function passed to collection.map() or .iterate(), and the call
// uses one of the function arguments. The argument will be unbound
// outside of the map operation and cannot be evaluated. See the Count
// Functions case in customfunction.js for details on the unboundName
// mechanism.
// TODO(user): Report the name of the offending argument.
throw new Error(`A mapped function's arguments (${
this.name()}) cannot be used in client-side operations.`);
} else {
throw new Error(
`Invalid cast to ${this.name()} from a client-side object.`);
}
}
return ee.rpc_node.argumentReference(name);
} else {
Expand Down
1 change: 1 addition & 0 deletions javascript/src/customfunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ ee.CustomFunction.variable = function(type, name) {
this.func = null;
this.args = null;
this.varName = name;
this.unbound = true;
};
klass.prototype = type.prototype;
return new klass(name);
Expand Down
9 changes: 6 additions & 3 deletions javascript/src/dictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,19 @@ ee.Dictionary = function(opt_dict) {
if (ee.Types.isRegularObject(opt_dict)) {
// Cast to a dictionary.
ee.Dictionary.base(this, 'constructor', null, null);
this.dict_ = /** @type {Object} */ (opt_dict);
this.dict_ = /** @type {!Object} */ (opt_dict);
} else {
if (opt_dict instanceof ee.ComputedObject && opt_dict.func &&
opt_dict.func.getSignature()['returns'] == 'Dictionary') {
// If it's a call that's already returning a Dictionary, just cast.
ee.Dictionary.base(this, 'constructor', opt_dict.func, opt_dict.args, opt_dict.varName);
ee.Dictionary.base(this, 'constructor', opt_dict.func, opt_dict.args, opt_dict.varName, opt_dict.unbound);
} else {
const unbound =
opt_dict instanceof ee.ComputedObject ? opt_dict.unbound : null;
// Delegate everything else to the server-side constructor.
ee.Dictionary.base(
this, 'constructor', new ee.ApiFunction('Dictionary'), {'input': opt_dict}, null);
this, 'constructor', new ee.ApiFunction('Dictionary'),
{'input': opt_dict}, null, unbound);
}
this.dict_ = null;
}
Expand Down
5 changes: 3 additions & 2 deletions javascript/src/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ goog.requireType('ee.Function');
* @param {ee.Function} func The same argument as in ee.ComputedObject().
* @param {Object} args The same argument as in ee.ComputedObject().
* @param {string?=} opt_varName The same argument as in ee.ComputedObject().
* @param {boolean?=} opt_unbound The same argument as in ee.ComputedObject().
* @constructor
* @extends {ee.ComputedObject}
*/
ee.Element = function(func, args, opt_varName) {
ee.Element.base(this, 'constructor', func, args, opt_varName);
ee.Element = function(func, args, opt_varName, opt_unbound) {
ee.Element.base(this, 'constructor', func, args, opt_varName, opt_unbound);
ee.Element.initialize();
};
goog.inherits(ee.Element, ee.ComputedObject);
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ ee.Feature = function(geometry, opt_properties) {
});
} else if (geometry instanceof ee.ComputedObject) {
// A custom object to reinterpret as a Feature.
ee.Feature.base(this, 'constructor', geometry.func, geometry.args, geometry.varName);
ee.Feature.base(this, 'constructor', geometry.func, geometry.args, geometry.varName, geometry.unbound);
} else if (geometry['type'] == 'Feature') {
// Try to convert a GeoJSON Feature.
var properties = geometry['properties'] || {};
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ ee.Filter = function(opt_filter) {
}
} else if (opt_filter instanceof ee.ComputedObject) {
// Actual filter object.
ee.Filter.base(this, 'constructor', opt_filter.func, opt_filter.args, opt_filter.varName);
ee.Filter.base(this, 'constructor', opt_filter.func, opt_filter.args, opt_filter.varName, opt_filter.unbound);
this.filter_ = [opt_filter];
} else if (opt_filter === undefined) {
// A silly call with no arguments left for backward-compatibility.
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ ee.Geometry = function(geoJson, opt_proj, opt_geodesic, opt_evenOdd) {
'Setting the CRS, geodesic, or evenOdd flag on a computed Geometry ' +
'is not supported. Use Geometry.transform().');
} else {
ee.Geometry.base(this, 'constructor', geoJson.func, geoJson.args, geoJson.varName);
ee.Geometry.base(this, 'constructor', geoJson.func, geoJson.args, geoJson.varName, geoJson.unbound);
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ ee.Image = function(opt_args) {
{'value': opt_args});
} else {
// A custom object to reinterpret as an Image.
ee.Image.base(this, 'constructor', opt_args.func, opt_args.args, opt_args.varName);
ee.Image.base(this, 'constructor', opt_args.func, opt_args.args, opt_args.varName, opt_args.unbound);
}
} else {
throw Error('Unrecognized argument type to convert to an Image: ' +
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/imagecollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ ee.ImageCollection = function(args) {
});
} else if (args instanceof ee.ComputedObject) {
// A custom object to reinterpret as an ImageCollection.
ee.ImageCollection.base(this, 'constructor', args.func, args.args, args.varName);
ee.ImageCollection.base(this, 'constructor', args.func, args.args, args.varName, args.unbound);
} else {
throw Error('Unrecognized argument type to convert to an ' +
'ImageCollection: ' + args);
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ ee.List = function(list) {
ee.List.base(this, 'constructor', null, null);
this.list_ = /** @type {IArrayLike} */ (list);
} else if (list instanceof ee.ComputedObject) {
ee.List.base(this, 'constructor', list.func, list.args, list.varName);
ee.List.base(this, 'constructor', list.func, list.args, list.varName, list.unbound);
this.list_ = null;
} else {
throw Error('Invalid argument specified for ee.List(): ' + list);
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ee.Number = function(number) {
ee.Number.base(this, 'constructor', null, null);
this.number_ = /** @type {number} */ (number);
} else if (number instanceof ee.ComputedObject) {
ee.Number.base(this, 'constructor', number.func, number.args, number.varName);
ee.Number.base(this, 'constructor', number.func, number.args, number.varName, number.unbound);
this.number_ = null;
} else {
throw Error('Invalid argument specified for ee.Number(): ' + number);
Expand Down
4 changes: 2 additions & 2 deletions javascript/src/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ ee.String = function(string) {
this.string_ = null;
if (string.func && string.func.getSignature()['returns'] == 'String') {
// If it's a call that's already returning a String, just cast.
ee.String.base(this, 'constructor', string.func, string.args, string.varName);
ee.String.base(this, 'constructor', string.func, string.args, string.varName, string.unbound);
} else {
ee.String.base(this, 'constructor', new ee.ApiFunction('String'), {'input': string}, null);
ee.String.base(this, 'constructor', new ee.ApiFunction('String'), {'input': string}, null, string.unbound);
}
} else {
throw Error('Invalid argument specified for ee.String(): ' + string);
Expand Down

0 comments on commit 95b0277

Please sign in to comment.