src/coverage/transform.js
import { COVERAGE, DOMAIN } from '../constants.js'
import { checkCoverage } from '../validate.js'
import { shallowcopy } from '../util.js'
import { addLoadRangesFunction } from './create.js'
/**
* Returns a copy of the given Coverage object with the parameters
* replaced by the supplied ones.
*
* Note that this is a low-level function and no checks are done on the supplied parameters.
*/
export function withParameters (cov, params) {
let newcov = {
type: COVERAGE,
domainType: cov.domainType,
parameters: params,
loadDomain: () => cov.loadDomain(),
loadRange: key => cov.loadRange(key),
loadRanges: keys => cov.loadRanges(keys),
subsetByIndex: constraints => cov.subsetByIndex(constraints).then(sub => withParameters(sub, params)),
subsetByValue: constraints => cov.subsetByValue(constraints).then(sub => withParameters(sub, params))
}
return newcov
}
/**
* Returns a copy of the given Coverage object with the categories
* of a given parameter replaced by the supplied ones and the encoding
* adapted to the given mapping from old to new.
*
* @param {Coverage} cov The Coverage object.
* @param {String} key The key of the parameter to work with.
* @param {object} observedProperty The new observed property including the new array of category objects
* that will be part of the returned coverage.
* @param {Map<String,String>} mapping A mapping from source category id to destination category id.
* @returns {Coverage}
*/
export function withCategories (cov, key, observedProperty, mapping) {
/* check breaks with Babel, see https://github.com/jspm/jspm-cli/issues/1348
if (!(mapping instanceof Map)) {
throw new Error('mapping parameter must be a Map from/to category ID')
}
*/
checkCoverage(cov)
if (observedProperty.categories.some(c => !c.id)) {
throw new Error('At least one category object is missing the "id" property')
}
let newparams = shallowcopy(cov.parameters)
let newparam = shallowcopy(newparams.get(key))
newparams.set(key, newparam)
newparams.get(key).observedProperty = observedProperty
let fromCatEnc = cov.parameters.get(key).categoryEncoding
let catEncoding = new Map()
let categories = observedProperty.categories
for (let category of categories) {
let vals = []
for (let [fromCatId, toCatId] of mapping) {
if (toCatId === category.id && fromCatEnc.has(fromCatId)) {
vals.push(...fromCatEnc.get(fromCatId))
}
}
if (vals.length > 0) {
catEncoding.set(category.id, vals)
}
}
newparams.get(key).categoryEncoding = catEncoding
let newcov = withParameters(cov, newparams)
return newcov
}
/**
* Returns a new coverage where the domainType field of the coverage and the domain
* is set to the given one.
*
* @param {Coverage} cov The Coverage object.
* @param {String} domainType The new domain type.
* @returns {Coverage}
*/
export function withDomainType (cov, domainType) {
checkCoverage(cov)
let domainWrapper = domain => {
let newdomain = {
type: DOMAIN,
domainType,
axes: domain.axes,
referencing: domain.referencing
}
return newdomain
}
let newcov = {
type: COVERAGE,
domainType,
parameters: cov.parameters,
loadDomain: () => cov.loadDomain().then(domainWrapper),
loadRange: key => cov.loadRange(key),
loadRanges: keys => cov.loadRanges(keys),
subsetByIndex: constraints => cov.subsetByIndex(constraints).then(sub => withDomainType(sub, domainType)),
subsetByValue: constraints => cov.subsetByValue(constraints).then(sub => withDomainType(sub, domainType))
}
return newcov
}
/**
* Tries to transform the given Coverage object into a new one that
* conforms to one of the CovJSON domain types.
* If multiple domain types match, then the "smaller" one is preferred,
* for example, Point instead of Grid.
*
* The transformation consists of:
* - Setting domainType in coverage and domain object
* - Renaming domain axes
*
* @see https://github.com/Reading-eScience-Centre/coveragejson/blob/master/domain-types.md
*
* @param {Coverage} cov The Coverage object.
* @returns {Promise<Coverage>}
* A Promise succeeding with the transformed coverage,
* or failing if no CovJSON domain type matched the input coverage.
*/
export function asCovJSONDomainType (cov) {
return cov.loadDomain().then(domain => {
// TODO implement me
})
}
/**
* @example
* var cov = ...
* var mapping = new Map()
* mapping.set('lat', 'y').set('lon', 'x')
* var newcov = CovUtils.renameAxes(cov, mapping)
*
* @param {Coverage} cov The coverage.
* @param {Map<String,String>} mapping
* @returns {Coverage}
*/
export function renameAxes (cov, mapping) {
checkCoverage(cov)
mapping = new Map(mapping)
for (let axisName of cov.axes.keys()) {
if (!mapping.has(axisName)) {
mapping.set(axisName, axisName)
}
}
let domainWrapper = domain => {
let newaxes = new Map()
for (let [from, to] of mapping) {
let {dataType, components, values, bounds} = domain.axes.get(from)
let newaxis = {
key: to,
dataType,
components: components.map(c => mapping.has(c) ? mapping.get(c) : c),
values,
bounds
}
newaxes.set(to, newaxis)
}
let newreferencing = domain.referencing.map(({components, system}) => ({
components: components.map(c => mapping.has(c) ? mapping.get(c) : c),
system
}))
let newdomain = {
type: DOMAIN,
domainType: domain.domainType,
axes: newaxes,
referencing: newreferencing
}
return newdomain
}
// pre-compile for efficiency
// get({['lat']: obj['y'], ['lon']: obj['x']})
let getObjStr = [...mapping].map(([from, to]) => `['${from}']:obj['${to}']`).join(',')
let rangeWrapper = range => {
let get = new Function('range', 'return function get (obj){return range.get({' + getObjStr + '})}')(range) // eslint-disable-line
let newrange = {
shape: new Map([...range.shape].map(([name, len]) => [mapping.get(name), len])),
dataType: range.dataType,
get
}
return newrange
}
let loadRange = paramKey => cov.loadRange(paramKey).then(rangeWrapper)
let loadRanges = paramKeys => cov.loadRanges(paramKeys)
.then(ranges => new Map([...ranges].map(([paramKey, range]) => [paramKey, rangeWrapper(range)])))
let newcov = {
type: COVERAGE,
domainType: cov.domainType,
parameters: cov.parameters,
loadDomain: () => cov.loadDomain().then(domainWrapper),
loadRange,
loadRanges,
subsetByIndex: constraints => cov.subsetByIndex(constraints).then(sub => renameAxes(sub, mapping)),
subsetByValue: constraints => cov.subsetByValue(constraints).then(sub => renameAxes(sub, mapping))
}
return newcov
}
/**
* @param {Coverage} cov The coverage.
* @param {String} key The key of the parameter for which the mapping should be applied.
* @param {Function} fn A function getting called as fn(obj, range) where obj is the axis indices object
* and range is the original range object.
* @param {String} [dataType] The new data type to use for the range. If omitted, the original type is used.
* @returns {Coverage}
*/
export function mapRange (cov, key, fn, dataType) {
checkCoverage(cov)
let rangeWrapper = range => {
let newrange = {
shape: range.shape,
dataType: dataType || range.dataType,
get: obj => fn(obj, range)
}
return newrange
}
let loadRange = paramKey => key === paramKey ? cov.loadRange(paramKey).then(rangeWrapper) : cov.loadRange(paramKey)
let loadRanges = paramKeys => cov.loadRanges(paramKeys)
.then(ranges => new Map([...ranges].map(([paramKey, range]) => [paramKey, key === paramKey ? rangeWrapper(range) : range])))
let newcov = {
type: COVERAGE,
domainType: cov.domainType,
parameters: cov.parameters,
loadDomain: () => cov.loadDomain(),
loadRange,
loadRanges,
subsetByIndex: constraints => cov.subsetByIndex(constraints).then(sub => mapRange(sub, key, fn, dataType)),
subsetByValue: constraints => cov.subsetByValue(constraints).then(sub => mapRange(sub, key, fn, dataType))
}
return newcov
}
/**
*
* @example
* var cov = ... // has parameters 'NIR', 'red', 'green', 'blue'
* var newcov = CovUtils.withDerivedParameter(cov, {
* parameter: {
* key: 'NDVI',
* observedProperty: {
* label: { en: 'Normalized Differenced Vegetation Index' }
* }
* },
* inputParameters: ['NIR','red'],
* dataType: 'float',
* fn: function (obj, nirRange, redRange) {
* var nir = nirRange.get(obj)
* var red = redRange.get(obj)
* if (nir === null || red === null) return null
* return (nir - red) / (nir + red)
* }
* })
*/
export function withDerivedParameter (cov, options) {
checkCoverage(cov)
let {parameter, inputParameters, dataType = 'float', fn} = options
let parameters = new Map(cov.parameters)
parameters.set(parameter.key, parameter)
let loadDerivedRange = () => cov.loadRanges(inputParameters).then(inputRanges => {
let inputRangesArr = inputParameters.map(key => inputRanges.get(key))
let shape = inputRangesArr[0].shape
let range = {
shape,
dataType,
get: obj => fn(obj, ...inputRangesArr)
}
return range
})
let loadRange = paramKey => parameter.key === paramKey ? loadDerivedRange() : cov.loadRange(paramKey)
let newcov = {
type: COVERAGE,
domainType: cov.domainType,
parameters,
loadDomain: () => cov.loadDomain(),
loadRange,
subsetByIndex: constraints => cov.subsetByIndex(constraints).then(sub => withDerivedParameter(sub, options)),
subsetByValue: constraints => cov.subsetByValue(constraints).then(sub => withDerivedParameter(sub, options))
}
addLoadRangesFunction(newcov)
return newcov
}
/**
*
* @example
* var cov = ... // has parameters 'NIR', 'red', 'green', 'blue'
* var newcov = CovUtils.withSimpleDerivedParameter(cov, {
* parameter: {
* key: 'NDVI',
* observedProperty: {
* label: { en: 'Normalized Differenced Vegetation Index' }
* }
* },
* inputParameters: ['NIR','red'],
* dataType: 'float',
* fn: function (nir, red) {
* return (nir - red) / (nir + red)
* }
* })
*/
export function withSimpleDerivedParameter (cov, options) {
let {parameter, inputParameters, dataType, fn} = options
let options_ = {
parameter,
inputParameters,
dataType,
// TODO pre-compile if too slow
fn: (obj, ...ranges) => {
let vals = inputParameters.map((_, i) => ranges[i].get(obj))
if (vals.some(val => val === null)) {
return null
}
return fn(...vals)
}
}
return withDerivedParameter(cov, options_)
}