Skip to content

Commit

Permalink
Handle unbound arguments from map and iterate calls.
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 2, 2024
1 parent f2e8070 commit 1fdc0f1
Show file tree
Hide file tree
Showing 12 changed files with 52 additions and 28 deletions.
23 changes: 16 additions & 7 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 Expand Up @@ -219,8 +220,12 @@ ee.Collection.prototype.elementType = function() {
* @export
*/
ee.Collection.prototype.map = function(algorithm, opt_dropNulls) {
var elementType = this.elementType();
var withCast = function(e) { return algorithm(new elementType(e)); };
const elementType = this.elementType();
const withCast = function(e) {
const el = /** @type {!ee.ComputedObject} */ (new elementType(e));
el.unbound = true;
return algorithm(el);
};
return this.castInternal(ee.ApiFunction._call(
'Collection.map', this, withCast, opt_dropNulls));
};
Expand All @@ -241,8 +246,12 @@ ee.Collection.prototype.map = function(algorithm, opt_dropNulls) {
* @export
*/
ee.Collection.prototype.iterate = function(algorithm, opt_first) {
var first = (opt_first !== undefined) ? opt_first : null;
var elementType = this.elementType();
var withCast = function(e, p) { return algorithm(new elementType(e), p); };
const first = (opt_first !== undefined) ? opt_first : null;
const elementType = this.elementType();
const withCast = function(e, p) {
const el = /** @type {!ee.ComputedObject} */ (new elementType(e));
el.unbound = true;
return algorithm(el, p);
};
return ee.ApiFunction._call('Collection.iterate', this, withCast, first);
};
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
2 changes: 1 addition & 1 deletion javascript/src/dictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ ee.Dictionary = function(opt_dict) {
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 {
// Delegate everything else to the server-side constructor.
ee.Dictionary.base(
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 1fdc0f1

Please sign in to comment.