diff --git a/javascript/src/collection.js b/javascript/src/collection.js index 6524f29fd..2af3bec7b 100644 --- a/javascript/src/collection.js +++ b/javascript/src/collection.js @@ -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); @@ -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)); }; @@ -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); }; diff --git a/javascript/src/computedobject.js b/javascript/src/computedobject.js index 9be9616e1..d134802c9 100644 --- a/javascript/src/computedobject.js +++ b/javascript/src/computedobject.js @@ -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); @@ -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. @@ -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 { diff --git a/javascript/src/dictionary.js b/javascript/src/dictionary.js index 54702e3d6..20ae5e4ad 100644 --- a/javascript/src/dictionary.js +++ b/javascript/src/dictionary.js @@ -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( diff --git a/javascript/src/element.js b/javascript/src/element.js index 5dd736fe9..e769ffe62 100644 --- a/javascript/src/element.js +++ b/javascript/src/element.js @@ -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); diff --git a/javascript/src/feature.js b/javascript/src/feature.js index 2d4ecd5dc..12195a471 100644 --- a/javascript/src/feature.js +++ b/javascript/src/feature.js @@ -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'] || {}; diff --git a/javascript/src/filter.js b/javascript/src/filter.js index be20ddbb6..4cd080394 100644 --- a/javascript/src/filter.js +++ b/javascript/src/filter.js @@ -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. diff --git a/javascript/src/geometry.js b/javascript/src/geometry.js index 0626ec9f5..bea753d06 100644 --- a/javascript/src/geometry.js +++ b/javascript/src/geometry.js @@ -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; } } diff --git a/javascript/src/image.js b/javascript/src/image.js index 041acf986..482cc6a06 100644 --- a/javascript/src/image.js +++ b/javascript/src/image.js @@ -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: ' + diff --git a/javascript/src/imagecollection.js b/javascript/src/imagecollection.js index 7d9a1031c..59fa4b2da 100644 --- a/javascript/src/imagecollection.js +++ b/javascript/src/imagecollection.js @@ -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); diff --git a/javascript/src/list.js b/javascript/src/list.js index 4fac2d91b..6a439ddd5 100644 --- a/javascript/src/list.js +++ b/javascript/src/list.js @@ -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); diff --git a/javascript/src/number.js b/javascript/src/number.js index d689c1c46..f8f2dbe26 100644 --- a/javascript/src/number.js +++ b/javascript/src/number.js @@ -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); diff --git a/javascript/src/string.js b/javascript/src/string.js index 0fba3bbfe..798228a19 100644 --- a/javascript/src/string.js +++ b/javascript/src/string.js @@ -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);