Source: migrate/collections.js

const Utils = require('../utils.js');
const Versions = require('../versions.js');
const MigrateCommons = require('./commons.js');

const extMap = {
    "cube": "datacube",
    "eo": "eo",
    "label": "label",
    "pc": "pointcloud",
    "proj": "projection",
    "sar": "sar",
    "sat": "sat",
    "sci": "scientific",
    "view": "view"
};

const fieldMap = {
    // Item to core
    'item:license': 'license',
    'item:providers': 'providers',
    // EO to core
    'eo:instrument': 'instruments',
    'eo:platform': 'platform',
    'eo:constellation': 'constellation',
    // EO to proj
    'eo:epsg': 'proj:epsg',
    // EO to view
    'eo:off_nadir': 'view:off_nadir',
    'eo:azimuth': 'view:azimuth',
    'eo:sun_azimuth': 'view:sun_azimuth',
    'eo:sun_elevation': 'view:sun_elevation',
    // Datetime Range to core
    'dtr:start_datetime': 'start_datetime',
    'dtr:end_datetime': 'end_datetime',
    // Point Cloud
    'pc:schema': 'pc:schemas',
    // SAR rename
    'sar:type': 'sar:product_type',
    'sar:polarization': 'sar:polarizations',
    // SAR to core
    'sar:instrument': 'instruments',
    'sar:platform': 'platform',
    'sar:constellation': 'constellation',
    // SAR to sat
    'sar:off_nadir': 'sat:off_nadir_angle',
    'sar:relative_orbit': 'sat:relative_orbit',
// The following four fields don't translate directly, see code below
    'sar:pass_direction': 'sat:orbit_state',
//   sar:resolution => sar:resolution_range, sar:resolution_azimuth
//   sar:pixel_spacing => sar:pixel_spacing_range, sar:pixel_spacing_azimuth
//   sar:looks => sar:looks_range, sar:looks_azimuth, sar:looks_equivalent_number (opt)
};

const moveToRoot = [
    'cube:dimensions',
    'sci:publications',
    'sci:doi',
    'sci:citation'
];

const DIMENSION_TYPES = [
    'spatial',
    'temporal',
    'bands',
    'other'
];


/** Migrate Collections related responses to the latest version. */
class MigrateCollections {

    /**
     * Converts a `GET /collections` response to the latest version.
     * 
     * Always returns a deep copy of the input object.
     * 
     * @param {object} response - The response to convert
     * @param {string} version - Version number of the API, which the response conforms to
     * @returns {object}
     */
    static convertCollectionsToLatestSpec(response, version) {
        if (Versions.compare(version, "0.3.x", "<=")) {
            throw "Migrating from API version 0.3.0 and older is not supported.";
        }

        // Make sure we don't alter the original object
        response = Utils.deepClone(response);

        if (Array.isArray(response.collections)) {
            response.collections = response.collections
                .map(c => MigrateCollections.convertCollectionToLatestSpec(c, version))
                .filter(c => typeof c.id === 'string');
        }
        else {
            response.collections = [];
        }

        response.links = MigrateCommons.migrateLinks(response.links, version);

        return response;
    }

    /**
     * Converts a single collection to the latest version.
     * 
     * Always returns a deep copy of the input object.
     * 
     * @param {object} process - The collection to convert
     * @param {string} version - Version number of the API, which the collection conforms to
     * @returns {object}
     */
    static convertCollectionToLatestSpec(originalCollection, version) {
        if (Versions.compare(version, "0.3.x", "<=")) {
            throw "Migrating from API version 0.3.0 and older is not supported.";
        }

        // Make sure we don't alter the original object
        let collection = Utils.deepClone(originalCollection);

        // If collection has no id => seems to be an invalid collection => abort
        if (typeof collection.id !== 'string' || collection.id.length === 0) {
            return {};
        }

        // Update stac_version
        if (!Versions.validate(collection.stac_version) || Versions.compare(collection.stac_version, "0.9.0", "<")) {
            collection.stac_version = "0.9.0";
        }

        // Add missing extent upfront. Makes the following code simpler as it works on the object.
        if (!Utils.isObject(collection.extent)) {
            collection.extent = {};
        }

        // convert v0.4 collections to latest version
        if (Versions.compare(version, "0.4.x", "=")) {
            // Restructure spatial extent
            if (Array.isArray(collection.extent.spatial)) {
                collection.extent.spatial = {
                    bbox: [
                        collection.extent.spatial
                    ]
                };
            }
            // Restructure temporal extent
            if (Array.isArray(collection.extent.temporal)) {
                collection.extent.temporal = {
                    interval: [
                        collection.extent.temporal
                    ]
                };
            }

            // move properties to other_properties
            if (Utils.isObject(collection.properties)) {
                if (!Utils.isObject(collection.other_properties)) {
                    collection.other_properties = {};
                }
                for(let key in collection.properties) {
                    collection.other_properties[key] = {
                        values: [
                            collection.properties[key]
                        ]
                    };
                }
            }
            delete collection.properties;

            // now we can work on all properties and migrate to summaries
            let props = Utils.isObject(collection.other_properties) ? collection.other_properties : {};
            for(let key in props) {
                let val = props[key];
                if (Utils.isObject(val) && (Array.isArray(val.extent) || Array.isArray(val.values))) {
                    if (Array.isArray(val.extent)) {
                        props[key] = {
                            min: val.extent[0],
                            max: val.extent[1]
                        };
                    }
                    else { // val.values is an array
                        if (val.values.findIndex(v => !Array.isArray(v)) === -1) {
                            if (val.values.length <= 1) {
                                props[key] = val.values[0];
                            }
                            else {
                                props[key] = val.values.reduce((a, b) => a.concat(b));
                            }
                        }
                        else {
                            props[key] = val.values;
                        }
                    }
                }
                else {
                    // If not valid, move to top-level
                    if (typeof collection[key] === 'undefined') {
                        collection[key] = val;
                    }
                    delete props[key];
                }
            }
            delete collection.other_properties;

            if (!Utils.isObject(collection.summaries)) {
                collection.summaries = {};
            }
            for(let key in props) {
                let val = props[key];

                if (key === 'sar:pass_direction') {
                    // Convert null to geostationary
                    val = val.map(v => v === null ? 'geostationary' : v);
                }

                // Convert arrays into separate fields as needed for some SAR fields
                if ((key === 'sar:resolution' || key === 'sar:pixel_spacing' || key === 'sar:looks') && Array.isArray(val) && val.length >= 2) {
                    collection.summaries[key + '_range'] = val.slice(0,1);
                    collection.summaries[key + '_azimuth'] = val.slice(1,2);
                    if (val.length > 2) {
                        collection.summaries[key + '_equivalent_number'] = val.slice(2,3);
                    }
                }
                // Do the renaming of fields
                else if (typeof fieldMap[key] === 'string') {
                    collection.summaries[fieldMap[key]] = val;
                }
                // Move invalid summaries to the top level
                else if (moveToRoot.includes(key) && Array.isArray(val) && val.length === 1) {
                    collection[key] = val[0];
                }
                // Do the general conversion
                else {
                    collection.summaries[key] = val;
                }
            }
        }

        // Add missing required fields
        if (typeof collection.description !== 'string') {
            collection.description = "";
        }
        if (!Utils.isObject(collection.extent.spatial)) {
            collection.extent.spatial = {};
        }
        if (!Utils.isObject(collection.extent.temporal)) {
            collection.extent.temporal = {};
        }
        if (typeof collection.license !== 'string') {
            collection.license = "proprietary";
        }
        if (!Utils.isObject(collection.summaries)) {
            collection.summaries = {};
        }
        if (!Utils.isObject(collection['cube:dimensions'])) {
            collection['cube:dimensions'] = {};
        }
        else {
            for(var name in collection['cube:dimensions']) {
                if (Utils.isObject(collection['cube:dimensions'][name]) && !DIMENSION_TYPES.includes(collection['cube:dimensions'][name].type)) {
                    collection['cube:dimensions'][name].type = 'other';
                }
            }
        }

        // Fix links
        collection.links = MigrateCommons.migrateLinks(collection.links);

        // Fix stac_extensions
        var extensions = Array.isArray(collection.stac_extensions) ? collection.stac_extensions : [];
        for(var key in collection) {
            let ext = null;
            let prefix = key.split(':', 1);
            if (key === 'deprecated' || key === 'version') {
                ext = 'version';
            }
            else if (typeof extMap[prefix] === 'string') {
                ext = extMap[prefix];
            }

            if (ext !== null && !extensions.includes(ext)) {
                extensions.push(ext);
            }
        }
        extensions.sort();
        collection.stac_extensions = extensions;

        return collection;
    }

}

module.exports = MigrateCollections;