Google Earth Engine Authors 29c70f21e2 v0.1.346
PiperOrigin-RevId: 517220709
2023-03-22 21:54:01 +00:00

1173 lines
35 KiB
JavaScript

/**
* @fileoverview An interface implemented by serializable objects.
*/
goog.provide('ee.Encodable');
goog.provide('ee.rpc_convert');
goog.provide('ee.rpc_node');
goog.require('ee.api');
goog.require('ee.apiclient');
goog.require('goog.array');
/**
* An interface implemented by objects that know how to serialize themselves.
* Not an actual Closure interface because we need instanceof to work.
* @constructor
*/
ee.Encodable = function() {};
/**
* Encodes an object in a format compatible with ee.Serializer.encode().
* @param {function(*): *} encoder A function that can be called to encode
* the components of an object.
* @return {*} The encoded form of the object.
*/
ee.Encodable.prototype.encode = goog.abstractMethod;
/**
* Defines the serializer behavior needed by encodeCloudValue.
* @typedef {{
* makeReference: function(*):string, unboundName:(string|undefined)}}
*/
ee.Encodable.Serializer;
/**
* Encodes an object in a format compatible with ee.Serializer.encodeCloudApi().
* @param {!ee.Encodable.Serializer}
* serializer An object that can be used to encode a serializable object.
* @return {!ee.api.ValueNode} The encoded object.
*/
ee.Encodable.prototype.encodeCloudValue = goog.abstractMethod;
/**
* Holds utility functions to build javascript protocol buffer objects.
*/
ee.rpc_node = {};
/**
* @param {*} obj
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.constant = function(obj) {
if (obj === undefined || obj === null) {
obj = ee.apiclient.NULL_VALUE;
}
return new ee.api.ValueNode({constantValue: obj});
};
/**
* @param {string} ref
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.reference = function(ref) {
return new ee.api.ValueNode({valueReference: ref});
};
/**
* @param {!Array<!ee.api.ValueNode>} values
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.array = function(values) {
return new ee.api.ValueNode({arrayValue: new ee.api.ArrayValue({values})});
};
/**
* @param {!Object<string, !ee.api.ValueNode>} values
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.dictionary = function(values) {
return new ee.api.ValueNode(
{dictionaryValue: new ee.api.DictionaryValue({values})});
};
/**
* @param {string} name
* @param {!Object<string, !ee.api.ValueNode>} args
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.functionByName = function(name, args) {
return new ee.api.ValueNode(
{functionInvocationValue: new ee.api.FunctionInvocation(
{functionName: name, arguments: args})});
};
/**
* @param {string} ref
* @param {!Object<string, !ee.api.ValueNode>} args
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.functionByReference = function(ref, args) {
return new ee.api.ValueNode(
{functionInvocationValue: new ee.api.FunctionInvocation(
{functionReference: ref, arguments: args})});
};
/**
* @param {!Array<string>} argumentNames
* @param {string} body
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.functionDefinition = function(argumentNames, body) {
return new ee.api.ValueNode(
{functionDefinitionValue: new ee.api.FunctionDefinition(
{argumentNames, body})});
};
/**
* @param {string} ref
* @return {!ee.api.ValueNode} constructed proto object.
*/
ee.rpc_node.argumentReference = function(ref) {
return new ee.api.ValueNode({argumentReference: ref});
};
/** Namespace to hold value conversion methods for the Cloud API calls. */
ee.rpc_convert = {};
/**
* @param {string|undefined} format
* @return {string}
*/
ee.rpc_convert.fileFormat = function(format) {
if (!format) {
return 'AUTO_JPEG_PNG';
}
const upper = format.toUpperCase();
switch (upper) {
case 'JPG':
return 'JPEG';
case 'AUTO':
return 'AUTO_JPEG_PNG';
case 'TIF':
case 'TIFF':
case 'GEOTIF':
case 'GEOTIFF':
return 'GEO_TIFF';
case 'TF_RECORD':
case 'TFRECORD':
return 'TF_RECORD_IMAGE';
case 'NUMPY':
return 'NPY';
case 'ZIPPED_TIF':
case 'ZIPPED_TIFF':
case 'ZIPPED_GEOTIF':
case 'ZIPPED_GEOTIFF':
return 'ZIPPED_GEO_TIFF';
case 'ZIPPED_TIF_PER_BAND':
case 'ZIPPED_TIFF_PER_BAND':
case 'ZIPPED_GEOTIF_PER_BAND':
case 'ZIPPED_GEOTIFF_PER_BAND':
return 'ZIPPED_GEO_TIFF_PER_BAND';
// It might be some other supported format. Let the server validate it.
default: return upper;
}
};
/**
* @param {string|undefined} format
* @return {string}
*/
ee.rpc_convert.tableFileFormat = function(format) {
if (!format) {
return 'CSV';
}
const upper = format.toUpperCase();
switch (upper) {
case 'TF_RECORD':
case 'TFRECORD':
return 'TF_RECORD_TABLE';
case 'JSON':
case 'GEOJSON':
return 'GEO_JSON';
// It might be some other supported format. Let the server validate it.
default:
return upper;
}
};
/**
* @param {string|undefined} orientation
* @return {string}
*/
ee.rpc_convert.orientation = function(orientation) {
if (!orientation) {
return 'VERTICAL';
}
const upper = orientation.toUpperCase();
if (upper !== 'HORIZONTAL' && upper !== 'VERTICAL') {
throw new Error('Orientation must be "horizontal" or "vertical"');
}
return upper;
};
/**
* @param {*} bands
* @return {!Array}
*/
ee.rpc_convert.bandList = function(bands) {
if (!bands) {
return [];
}
if (typeof bands === 'string') {
return bands.split(',');
}
if (Array.isArray(bands)) {
return bands;
}
throw new Error('Invalid band list ' + bands);
};
/**
* @param {!Object} params
* @return {?ee.api.VisualizationOptions}
*/
ee.rpc_convert.visualizationOptions = function(params) {
const result = new ee.api.VisualizationOptions();
let hasResult = false;
if ('palette' in params) {
const pal = params['palette'];
result.paletteColors = (typeof pal === 'string') ? pal.split(',') : pal;
hasResult = true;
}
let ranges = [];
if ('gain' in params || 'bias' in params) {
if ('min' in params || 'max' in params) {
throw new Error('Gain and bias can\'t be specified with min and max');
}
// Convert to min & max for the Cloud API.
const valueRange =
result.paletteColors ? result.paletteColors.length - 1 : 255;
ranges = ee.rpc_convert.pairedValues(params, 'bias', 'gain').map(pair => {
const min = -pair['bias'] / pair['gain'];
return {'min': min, 'max': valueRange / pair['gain'] + min };
});
} else if ('min' in params || 'max' in params) {
ranges = ee.rpc_convert.pairedValues(params, 'min', 'max');
}
if (ranges.length !== 0) {
result.ranges = ranges.map(range => new ee.api.DoubleRange(range));
hasResult = true;
}
const gammas = ee.rpc_convert.csvToNumbers(params['gamma']);
if (gammas.length > 1) {
throw new Error('Only one gamma value is supported');
} else if (gammas.length === 1) {
result.gamma = gammas[0];
hasResult = true;
}
return hasResult ? result : null;
};
/**
* @param {string|undefined} csv
* @return {!Array<number>}
*/
ee.rpc_convert.csvToNumbers = function(csv) {
if (!csv) {
return [];
}
return csv.split(',').map(Number);
};
/**
* Converts the `a` and `b` fields of obj to arrays of numbers, and returns an
* array of pairs of numbers. It is an error if the arrays are different
* lengths, but if either field is missing or empty, it is populated with
* default values of 0 and 1 for a and b respectively.
* @param {!Object} obj
* @param {string} a
* @param {string} b
* @return {!Array<!Object<string, number>>} Objects have `a` and `b` members.
*/
ee.rpc_convert.pairedValues = function(obj, a, b) {
let aValues = ee.rpc_convert.csvToNumbers(obj[a]);
let bValues = ee.rpc_convert.csvToNumbers(obj[b]);
if (aValues.length === 0) {
return bValues.map(value => ({[a]: 0, [b]: value}));
} else if (bValues.length === 0) {
return aValues.map(value => ({[a]: value, [b]: 1}));
} else if (aValues.length !== bValues.length) {
throw new Error('Length of ' + a + ' and ' + b + ' must match.');
}
return aValues.map((value, index) => ({[a]: value, [b]: bValues[index]}));
};
/**
* Converts a ListAlgorithmsResponse to the internal format.
*
* The internal code expects an object mapping each algorithm's name to an
* object containing:
* - description: string
* - returns: string
* - args: list of objects, each containing
* - name: argument name
* - type: argument type
* - description: argument description (optional)
* - optional: bool (optional)
* - default: default value (optional)
* - hidden: bool (optional)
* - preview: bool (optional)
* - deprecated: string containing deprecation reason (optional)
* - sourceCodeUri: string (optional)
*
* @param {!ee.api.ListAlgorithmsResponse} result
* @return {!Object}
*/
ee.rpc_convert.algorithms = function(result) {
const convertArgument = (argument) => {
const internalArgument = {};
internalArgument['description'] = argument.description || '';
internalArgument['type'] = argument.type || '';
if (argument.argumentName != null) {
internalArgument['name'] = argument.argumentName;
}
if (argument.defaultValue !== undefined) {
internalArgument['default'] = argument.defaultValue;
}
if (argument.optional != null) {
internalArgument['optional'] = argument.optional;
}
return internalArgument;
};
const convertAlgorithm = (algorithm) => {
const internalAlgorithm = {};
internalAlgorithm['args'] =
(algorithm.arguments || []).map(convertArgument);
internalAlgorithm['description'] = algorithm.description || '';
internalAlgorithm['returns'] = algorithm.returnType || '';
if (algorithm.hidden != null) {
internalAlgorithm['hidden'] = algorithm.hidden;
}
if (algorithm.preview) {
internalAlgorithm['preview'] = algorithm.preview;
}
if (algorithm.deprecated) {
internalAlgorithm['deprecated'] = algorithm.deprecationReason;
}
if (algorithm.sourceCodeUri) {
internalAlgorithm['sourceCodeUri'] = algorithm.sourceCodeUri;
}
return internalAlgorithm;
};
const internalAlgorithms = {};
for (const algorithm of result.algorithms || []) {
const name = algorithm.name.replace(/^algorithms\//, '');
internalAlgorithms[name] = convertAlgorithm(algorithm);
}
return internalAlgorithms;
};
/** @const {string} */
ee.rpc_convert.DEFAULT_PROJECT = 'earthengine-legacy';
/** @const {string} */
ee.rpc_convert.PUBLIC_PROJECT = 'earthengine-public';
/**
* @const {!RegExp} Matches a Cloud project ID in an asset path of the form
* "projects/PROJECT/...". Returns matching group $1 for the project ID.
* /\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:/ matches a Dasher/Cloud org prefix.
* /[a-z][a-z0-9\-]{4,28}[a-z0-9]/ matches a valid Cloud project ID.
*/
ee.rpc_convert.PROJECT_ID_RE =
/^projects\/((?:\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:)?[a-z][a-z0-9\-]{4,28}[a-z0-9])\/.+/;
/**
* @const {!RegExp} Matches a Cloud asset ID in an asset path of the form
* "projects/PROJECT/assets/(path)". Returns matching group $1 for the project
* ID and $2 for the asset path.
*/
ee.rpc_convert.CLOUD_ASSET_ID_RE =
/^projects\/((?:\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:)?[a-z][a-z0-9\-]{4,28}[a-z0-9])\/assets\/(.*)$/;
/**
* @const {!RegExp} Matches a Cloud asset root path of the form
* "projects/PROJECT/assets/?". Returns matching group $1 for the project ID.
*/
ee.rpc_convert.CLOUD_ASSET_ROOT_RE =
/^projects\/((?:\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:)?[a-z][a-z0-9\-]{4,28}[a-z0-9])\/assets\/?$/;
/**
* @param {string} path Resource path potentially containing a project ID.
* @return {string} The project ID to use for the given path.
*/
ee.rpc_convert.projectIdFromPath = function(path) {
const matches = ee.rpc_convert.PROJECT_ID_RE.exec(path);
if (matches) {
return matches[1];
}
// Default project for users without a Cloud project.
return ee.rpc_convert.DEFAULT_PROJECT;
};
/**
* @param {string} path Resource path potentially containing a project ID.
* @return {string} The project root ID to use for the given path.
*/
ee.rpc_convert.projectParentFromPath = function(path) {
return 'projects/' + ee.rpc_convert.projectIdFromPath(path);
};
/**
* @param {string} param The asset ID.
* @return {string} The full asset name.
*/
ee.rpc_convert.assetIdToAssetName = function(param) {
if (ee.rpc_convert.CLOUD_ASSET_ID_RE.exec(param)) {
return param;
} else if (/^(users|projects)\/.*/.exec(param)) {
return `projects/${ee.rpc_convert.DEFAULT_PROJECT}/assets/${param}`;
} else {
return `projects/${ee.rpc_convert.PUBLIC_PROJECT}/assets/${param}`;
}
};
/**
* Returns the asset ID without the 'projects/PROJECT/assets/' prefix. If
* `name` is not in this form or PROJECT is the default or public project,
* it is returned unmodified.
* @param {string} name asset name
* @return {string} asset id
*/
ee.rpc_convert.assetNameToAssetId = function(name) {
const parts = name.split('/');
const isLegacyProject =
(id) => [ee.rpc_convert.DEFAULT_PROJECT, ee.rpc_convert.PUBLIC_PROJECT]
.includes(id);
if (parts[0] === 'projects' && parts[2] === 'assets' &&
isLegacyProject(parts[1])) {
return parts.slice(3).join('/');
}
return name;
};
/**
* @param {?string} param
* @return {?string}
*/
ee.rpc_convert.assetTypeForCreate = function(param) {
switch (param) {
case 'ImageCollection': return 'IMAGE_COLLECTION';
case 'Folder': return 'FOLDER';
default: return param;
}
};
/**
* @param {!ee.api.ListAssetsResponse} result
* @return {!Object}
*/
ee.rpc_convert.listAssetsToGetList = function(result) {
return (result.assets || []).map(ee.rpc_convert.assetToLegacyResult);
};
/**
* @param {?string} type The cloud asset type. These types must match the values
* in google.earthengine.v1main.EarthEngineAsset.Type.
* @return {string} The equivalent legacy asset type.
*/
ee.rpc_convert.assetTypeToLegacyAssetType = function(type) {
switch (type) {
case 'ALGORITHM':
return 'Algorithm';
case 'FOLDER':
return 'Folder';
case 'IMAGE':
return 'Image';
case 'IMAGE_COLLECTION':
return 'ImageCollection';
case 'TABLE':
return 'Table';
case 'CLASSIFIER':
return 'Classifier';
case 'FEATURE_VIEW':
return 'FeatureView';
default:
return 'Unknown';
}
};
/**
* @param {?string} type The legacy asset type.
* @return {string} The equivalent cloud asset type.
*/
ee.rpc_convert.legacyAssetTypeToAssetType = function(type) {
switch (type) {
case 'Algorithm':
return 'ALGORITHM';
case 'Folder':
return 'FOLDER';
case 'Image':
return 'IMAGE';
case 'ImageCollection':
return 'IMAGE_COLLECTION';
case 'Table':
return 'TABLE';
default:
return 'UNKNOWN';
}
};
/**
* @param {!ee.api.EarthEngineAsset} result
* @return {!Object}
*/
ee.rpc_convert.assetToLegacyResult = function(result) {
const asset = ee.rpc_convert.makeLegacyAsset_(
ee.rpc_convert.assetTypeToLegacyAssetType(result.type), result.name);
const properties = Object.assign({}, result.properties || {});
// Put system properties back into the legacy properties object.
if (result.sizeBytes) {
properties['system:asset_size'] = Number(result.sizeBytes);
}
if (result.startTime) {
properties['system:time_start'] = Date.parse(result.startTime);
}
if (result.endTime) {
properties['system:time_end'] = Date.parse(result.endTime);
}
if (result.geometry) {
properties['system:footprint'] = result.geometry; // GeoJSON
}
if (typeof result.title === 'string') {
properties['system:title'] = result.title;
} else if (typeof properties['title'] === 'string') {
properties['system:title'] = properties['title'];
}
if (typeof result.description === 'string') {
properties['system:description'] = result.description;
} else if (typeof properties['description'] === 'string') {
properties['system:description'] = properties['description'];
}
if (result.updateTime) {
asset['version'] = Date.parse(result.updateTime) * 1000; // us
}
asset['properties'] = properties;
if (result.bands) {
asset['bands'] = result.bands.map((band) => {
const legacyBand = {
'id': band.id,
'crs': band.grid.crsCode,
'dimensions': undefined,
'crs_transform': undefined,
};
if (band.grid) {
if (band.grid.affineTransform != null) {
const affine = band.grid.affineTransform;
legacyBand['crs_transform'] = [
affine.scaleX || 0,
affine.shearX || 0,
affine.translateX || 0,
affine.shearY || 0,
affine.scaleY || 0,
affine.translateY || 0,
];
}
if (band.grid.dimensions != null) {
legacyBand['dimensions'] =
[band.grid.dimensions.width, band.grid.dimensions.height];
}
}
if (band.dataType) {
const dataType = {'type': 'PixelType'};
dataType['precision'] =
(band.dataType.precision || '').toLowerCase();
if (band.dataType.range) {
dataType['min'] = band.dataType.range.min || 0;
dataType['max'] = band.dataType.range.max;
}
legacyBand['data_type'] = dataType;
}
return legacyBand;
});
}
if (result.featureViewAssetLocation) {
asset['mapLocation'] = result.featureViewAssetLocation;
}
if (result.featureCount) {
asset['featureCount'] = result.featureCount;
}
return asset;
};
/**
* @param {!Object} legacyProperties
* @return {!ee.api.EarthEngineAsset}
*/
ee.rpc_convert.legacyPropertiesToAssetUpdate = function(legacyProperties) {
const asset = new ee.api.EarthEngineAsset();
const toTimestamp = (msec) => new Date(Number(msec)).toISOString();
const asNull = (value) =>
value === null ? /** type {?} */ (ee.apiclient.NULL_VALUE) : undefined;
const properties = Object.assign({}, legacyProperties);
let value;
const extractValue = (key) => {
value = properties[key];
delete properties[key];
return value;
};
// Extract the legacy properties from the properties object. May be set to
// null for deletion, but we use NULL_VALUE so that Serializable$has returns
// true.
if (extractValue('system:asset_size') !== undefined) {
asset.sizeBytes = asNull(value) || String(value);
}
if (extractValue('system:time_start') !== undefined) {
asset.startTime = asNull(value) || toTimestamp(value);
}
if (extractValue('system:time_end') !== undefined) {
asset.endTime = asNull(value) || toTimestamp(value);
}
if (extractValue('system:footprint') !== undefined) {
asset.geometry = asNull(value) || value;
}
// Extract `system:title` and set it in `properties` unless `title` is present
// in `properties`, which takes precedence.
extractValue('system:title');
if ((typeof value === 'string' || value === null) &&
properties['title'] == null) {
properties['title'] = asNull(value) || value;
}
// Extract `system:description` and set it in `properties` unless
// `description` is present in `properties`, which takes precedence.
extractValue('system:description');
if ((typeof value === 'string' || value === null) &&
properties['description'] == null) {
properties['description'] = asNull(value) || value;
}
// update_time cannot be set directly.
Object.entries(properties).forEach(([key, value]) => {
properties[key] = asNull(value) || value;
});
asset.properties = properties;
return asset;
};
/**
* @param {?string} type
* @param {?undefined|string} name
* @return {!Object}
* @private
*/
ee.rpc_convert.makeLegacyAsset_ = function(type, name) {
const legacyAsset = {};
legacyAsset['type'] = type;
if (name != null) {
legacyAsset['id'] = ee.rpc_convert.assetNameToAssetId(name);
}
return legacyAsset;
};
/**
* @param {!Object} param
* @return {!ee.api.ProjectsAssetsListAssetsNamedParameters}
*/
ee.rpc_convert.getListToListAssets = function(param) {
const assetsRequest = {};
const toTimestamp = (msec) => new Date(msec).toISOString();
if (param['num']) {
assetsRequest['pageSize'] = param['num'];
}
if (param['starttime']) {
assetsRequest['startTime'] = toTimestamp(param['starttime']);
}
if (param['endtime']) {
assetsRequest['endTime'] = toTimestamp(param['endtime']);
}
if (param['bbox']) {
assetsRequest['region'] =
ee.rpc_convert.boundingBoxToGeoJson(param['bbox']);
}
if (param['region']) {
assetsRequest['region'] = param['region'];
}
if (param['bbox'] && param['region']) {
console.warn('Multiple request parameters converted to region');
}
// The ID field is pulled into the "parent" param, the URL of the request.
const allKeys = ['id', 'num', 'starttime', 'endtime', 'bbox', 'region'];
for (let key of Object.keys(param).filter(k => !allKeys.includes(k))) {
console.warn('Unrecognized key ' + key + ' ignored');
}
return ee.rpc_convert.processListImagesParams(assetsRequest);
};
/**
* @param {!Array<number>} bbox
* @return {string}
*/
ee.rpc_convert.boundingBoxToGeoJson = function(bbox) {
const indexes = [[0, 1], [2, 1], [2, 3], [0, 3], [0, 1]];
const pairs = indexes.map(i => bbox[i[0]] + ',' + bbox[i[1]]);
return '{"type":"Polygon","coordinates":[[[' + pairs.join('],[') + ']]]}';
};
/**
* Converts an IamPolicy object to our internal AssetAcl representation.
* @param {!ee.api.Policy} result
* @return {!Object}
*/
ee.rpc_convert.iamPolicyToAcl = function(result) {
const bindingMap = {};
(result.bindings || []).forEach(binding => {
bindingMap[binding.role] = binding.members;
});
const groups = new Set();
const toAcl = (member) => {
const email =
member.replace(/^domain:|^group:|^serviceAccount:|^user:/, '');
if (member.startsWith('group:')) {
groups.add(email);
}
return email;
};
const readersWithAll = bindingMap['roles/viewer'] || [];
const readers = readersWithAll.filter(reader => reader !== 'allUsers');
const internalAcl = {
'owners': (bindingMap['roles/owner'] || []).map(toAcl),
'writers': (bindingMap['roles/editor'] || []).map(toAcl),
'readers': readers.map(toAcl),
};
if (groups.size > 0) {
internalAcl['groups'] = groups;
}
if (readersWithAll.length != readers.length) {
internalAcl['all_users_can_read'] = true;
}
return internalAcl;
};
/**
* @param {!Object} acls
* @return {!ee.api.Policy}
*/
ee.rpc_convert.aclToIamPolicy = function(acls) {
const hasPrefix = (email) => email.includes(':');
const isDomain = (email) => !email.includes('@');
const isGroup = (email) => acls['groups'] && acls['groups'].has(email);
const isServiceAccount = (email) =>
email.match(/[@|\.]gserviceaccount\.com$/);
// Converts the list of emails to <prefix>:<email> format for IamPolicy.
const asMembers = (aclName) => (acls[aclName] || []).map((email) => {
if (hasPrefix(email)) {
return email;
}
let prefix = 'user:';
if (isDomain(email)) {
prefix = 'domain:';
} else if (isGroup(email)) {
prefix = 'group:';
} else if (isServiceAccount(email)) {
prefix = 'serviceAccount:';
}
return prefix + email;
});
const all = acls['all_users_can_read'] ? ['allUsers'] : [];
const bindings = [
{role: 'roles/owner', members: asMembers('owners')},
{role: 'roles/viewer', members: asMembers('readers').concat(all)},
{role: 'roles/editor', members: asMembers('writers')},
].map(params => new ee.api.Binding(params));
return new ee.api.Policy({
bindings: bindings.filter(binding => binding.members.length),
etag: null,
});
};
/**
* @param {string} operationNameOrTaskId
* @return {string}
*/
ee.rpc_convert.taskIdToOperationName = function(operationNameOrTaskId) {
const taskId = ee.rpc_convert.operationNameToTaskId(operationNameOrTaskId);
const project = ee.rpc_convert.operationNameToProject(operationNameOrTaskId);
return `projects/${project}/operations/${taskId}`;
};
/**
* @param {string} result
* @return {string}
*/
ee.rpc_convert.operationNameToTaskId = function(result) {
const found = /^.*operations\/(.*)$/.exec(result);
return found ? found[1] : result;
};
/**
* @param {string} operationNameOrTaskId
* @return {string}
*/
ee.rpc_convert.operationNameToProject = function(operationNameOrTaskId) {
const found = /^projects\/(.+)\/operations\/.+$/.exec(operationNameOrTaskId);
return found ? found[1] : ee.rpc_convert.DEFAULT_PROJECT;
};
/**
* @param {!ee.api.Operation} result
* @return {!Object}
*/
ee.rpc_convert.operationToTask = function(result) {
const internalTask = {};
const assignTimestamp = (field, timestamp) => {
if (timestamp != null) {
internalTask[field] = Date.parse(timestamp);
}
};
const convertState = (state) => {
switch (state) {
case 'PENDING': return 'READY';
case 'RUNNING': return 'RUNNING';
case 'CANCELLING': return 'CANCEL_REQUESTED';
case 'SUCCEEDED': return 'COMPLETED';
case 'CANCELLED': return 'CANCELLED';
case 'FAILED': return 'FAILED';
default: return 'UNKNOWN';
}
};
const metadata = ee.apiclient.deserialize(
ee.api.OperationMetadata, result.metadata || {});
if (metadata.description != null) {
internalTask['description'] = metadata.description;
}
if (metadata.state != null) {
internalTask['state'] = convertState(metadata.state);
}
assignTimestamp('creation_timestamp_ms', metadata.createTime);
assignTimestamp('update_timestamp_ms', metadata.updateTime);
assignTimestamp('start_timestamp_ms', metadata.startTime);
internalTask['attempt'] = metadata.attempt;
if (result.done && result.error != null) {
internalTask['error_message'] = result.error.message;
}
if (result.name != null) {
internalTask['id'] = ee.rpc_convert.operationNameToTaskId(result.name);
internalTask['name'] = result.name;
}
internalTask['task_type'] = metadata.type || 'UNKNOWN';
internalTask['output_url'] = metadata.destinationUris;
internalTask['source_url'] = metadata.scriptUri;
return internalTask;
};
/**
* @param {!ee.api.Operation} operation
* @return {!Object} ProcessingResponse
*/
ee.rpc_convert.operationToProcessingResponse = function(operation) {
// The legacy response should always return OK. If the task is
// attempted to be created again with the same requestId an exception
// may be thrown if the request is not byte for byte identical.
const result = {'started': 'OK'};
if (operation.name) {
result['taskId'] = ee.rpc_convert.operationNameToTaskId(operation.name);
result['name'] = operation.name;
}
if (operation.error) {
result['note'] = operation.error.message;
}
return result;
};
/**
* @param {!Object} source Legacy source object.
* @return {?Array<string>} Paths to add to the uris field.
*/
ee.rpc_convert.sourcePathsToUris = function(source) {
if (source['primaryPath']) {
return [source['primaryPath'], ...source['additionalPaths'] || []];
}
return null;
};
/**
* @param {!Object} params Legacy request parameters
* @return {!ee.api.ImageManifest}
*/
ee.rpc_convert.toImageManifest = function(params) {
// Use deserialize to convert string-keyed objects to API types.
const convertImageSource = (source) => {
const apiSource = ee.apiclient.deserialize(ee.api.ImageSource, source);
apiSource.uris = ee.rpc_convert.sourcePathsToUris(source);
return apiSource;
};
const convertTileset = (tileset) => {
const apiTileset = ee.apiclient.deserialize(ee.api.Tileset, tileset);
apiTileset.sources = (tileset['sources'] || []).map(convertImageSource);
return apiTileset;
};
const convertBands = (band) => {
const apiBand = ee.apiclient.deserialize(ee.api.TilesetBand, band);
apiBand.missingData =
ee.rpc_convert.toOnePlatformMissingData(band['missingData']);
return apiBand;
};
// Retain existing keys
const manifest = ee.apiclient.deserialize(ee.api.ImageManifest, params);
// TODO(user): Transform keys as done in ee/cli/commands.py
manifest.name = ee.rpc_convert.assetIdToAssetName(params['id']);
manifest.tilesets = (params['tilesets'] || []).map(convertTileset);
manifest.bands = (params['bands'] || []).map(convertBands);
manifest.missingData =
ee.rpc_convert.toOnePlatformMissingData(params['missingData']);
manifest.maskBands = goog.array.flatten(
(params['tilesets'] || []).map(ee.rpc_convert.toOnePlatformMaskBands));
manifest.pyramidingPolicy = params['pyramidingPolicy'] || null;
// Extract properties.
if (params['properties']) {
const properties = Object.assign({}, params['properties']);
const toTimestamp = (msec) => new Date(Number(msec)).toISOString();
let value;
const extractValue = (key) => {
value = properties[key];
delete properties[key];
return value;
};
if (extractValue('system:time_start')) {
manifest.startTime = toTimestamp(value);
}
if (extractValue('system:time_end')) {
manifest.endTime = toTimestamp(value);
}
manifest.properties = properties;
}
return manifest;
};
/**
* Returns an array of TilesetMaskBands for a given tileset.
*
* @param {!Object} tileset legacy tileset object.
* @return {!Array<!ee.api.TilesetMaskBand>}
*/
ee.rpc_convert.toOnePlatformMaskBands = function(tileset) {
const maskBands = [];
if (!Array.isArray(tileset['fileBands'])) {
return maskBands;
}
/**
* Return a TilesetMaskBand for a given maskConfig. If the maskConfig
* is null the default is to return a TilesetMaskBand for all bands
* (No bandIds set).
*
* @param {?Object} maskConfig the old-manifest maskConfig.
* @return {!ee.api.TilesetMaskBand}
*/
const convertMaskConfig = (maskConfig) => {
let bandIds = [];
if (maskConfig != null && Array.isArray(maskConfig['bandId'])) {
bandIds = maskConfig['bandId'].map((bandId) => bandId || '');
}
// TODO(user): Tileset ID is always set to the default of the empty
// string, if we decide to set the tilesetId on the OnePlatform tileset
// we need to change this.
return new ee.api.TilesetMaskBand({tilesetId: tileset['id'] || '', bandIds});
};
tileset['fileBands'].forEach((fileBand) => {
if (fileBand['maskForAllBands']) {
// Provide null since that will return a default
// mask for all bands.
maskBands.push(convertMaskConfig(null));
} else if (fileBand['maskForBands'] != null) {
maskBands.push(convertMaskConfig(fileBand['maskForBands']));
}
});
return maskBands;
};
/**
* @param {!Object} params Legacy request parameters
* @return {!ee.api.TableManifest}
*/
ee.rpc_convert.toTableManifest = function(params) {
const convertTableSource = (source) => {
const apiSource = ee.apiclient.deserialize(ee.api.TableSource, source);
apiSource.uris = ee.rpc_convert.sourcePathsToUris(source);
if (source['maxError']) {
apiSource.maxErrorMeters = source['maxError'];
}
return apiSource;
};
// Retain existing keys
const manifest = ee.apiclient.deserialize(ee.api.TableManifest, params);
// TODO(user): Transform keys as done in ee/cli/commands.py
manifest.name = ee.rpc_convert.assetIdToAssetName(params['id']);
manifest.sources = (params['sources'] || []).map(convertTableSource);
// Extract properties.
if (params['properties']) {
const properties = Object.assign({}, params['properties']);
const toTimestamp = (msec) => new Date(Number(msec)).toISOString();
let value;
const extractValue = (key) => {
value = properties[key];
delete properties[key];
return value;
};
if (extractValue('system:time_start')) {
manifest.startTime = toTimestamp(value);
}
if (extractValue('system:time_end')) {
manifest.endTime = toTimestamp(value);
}
manifest.properties = properties;
}
return manifest;
};
/**
* @param {!Object} params Legacy image manifest missing data field.
* @return {?ee.api.MissingData}
*/
ee.rpc_convert.toOnePlatformMissingData = function(params) {
if (params == null) {
return null;
}
const missingData = new ee.api.MissingData({values: []});
if (params['value'] != null && typeof params['value'] === 'number') {
missingData.values.push(params['value']);
}
if (Array.isArray(params['values'])) {
params['values'].map(value => {
if (typeof value === 'number') {
missingData.values.push(value);
}
});
}
return (goog.array.isEmpty(missingData.values)) ? null : missingData;
};
/**
* @param {!ee.api.FolderQuota} quota
* @return {!Object}
*/
ee.rpc_convert.folderQuotaToAssetQuotaDetails = function(quota) {
const toNumber = (field) => Number(field || 0);
return {
asset_count: {
usage: toNumber(quota.assetCount),
limit: toNumber(quota.maxAssets),
},
asset_size: {
usage: toNumber(quota.sizeBytes),
limit: toNumber(quota.maxSizeBytes),
}
};
};
/**
* Processes the ee.data.listImages filter params into a single query string and
* removes them from the input params.
*
* @param {!Object} params
* @return {string}
* @private
*/
ee.rpc_convert.parseFilterParamsFromListImages_ = function(params) {
const queryStrings = [];
const toISOString = (dateStr) => new Date(dateStr).toISOString();
if ('startTime' in params) {
queryStrings.push(`startTime >= "${toISOString(params['startTime'])}"`);
delete params['startTime'];
}
if ('endTime' in params) {
queryStrings.push(`endTime < "${toISOString(params['endTime'])}"`);
delete params['endTime'];
}
const REGION_ERROR = 'Filter parameter "region" must be a ' +
'GeoJSON or WKT string.';
if ('region' in params) {
let region = params['region'];
if (typeof region === 'object') {
try {
region = JSON.stringify(region);
} catch (x) {
throw new Error(REGION_ERROR);
}
} else if (typeof region !== 'string') {
throw new Error(REGION_ERROR);
}
// Double quotes are not valid in the GeoJSON strings, since we wrap the
// query in a set of double quotes. We trivially avoid doubly-escaping the
// quotes by replacing double quotes with single quotes.
region = region.replace(/"/g, `'`);
queryStrings.push(`intersects("${region}")`);
delete params['region'];
}
if ('properties' in params) {
if (!Array.isArray(params['properties']) &&
params['properties'].some((property) => typeof property !== 'string')) {
throw new Error(
'Filter parameter "properties" must be an array of strings');
}
for (let propertyQuery of params['properties']) {
// Property filtering requires that properties be prefixed by
// "properties."
queryStrings.push(
propertyQuery.trim().replace(/^(properties\.)?/, 'properties.'));
}
delete params['properties'];
}
return queryStrings.join(' AND ');
};
/**
* Processes the ee.data.listImages params into ListAssets request params.
*
* @param {!Object} params
* @return {!ee.api.ProjectsAssetsListAssetsNamedParameters}
*/
ee.rpc_convert.processListImagesParams = function(params) {
params = Object.assign({}, params);
const extraFilters = ee.rpc_convert.parseFilterParamsFromListImages_(params);
if (extraFilters) {
if (params['filter']) {
params['filter'] = `${params['filter']} AND ${extraFilters}`;
} else {
params['filter'] = extraFilters;
}
}
return params;
};