Home Reference Source Repository

src/coverage/subset.js

import { isISODateAxis, isLongitudeAxis, getLongitudeWrapper, asTime } from '../domain/referencing.js'
import { normalizeIndexSubsetConstraints, subsetDomainByIndex } from '../domain/subset.js'
import { indexOfNearest, indicesOfNearest } from '../array.js'
import { COVERAGE } from '../constants.js'

/**
 * Returns a copy of the grid coverage subsetted to the given bounding box.
 *
 * Any grid cell is included which intersects with the bounding box.
 *
 * @param {Coverage} cov A Coverage object with domain Grid.
 * @param {array} bbox [xmin,ymin,xmax,ymax] in native CRS coordinates.
 * @param {array} [axes=['x','y']] Axis names [x,y].
 * @returns {Promise<Coverage>} A promise with a Coverage object as result.
 */
export function subsetByBbox (cov, bbox, axes = ['x', 'y']) {
  let [xmin, ymin, xmax, ymax] = bbox
  return cov.subsetByValue({[axes[0]]: {start: xmin, stop: xmax}, [axes[1]]: {start: ymin, stop: ymax}})
}

/**
 * Generic subsetByIndex function that can be used when building new Coverage objects.
 *
 * @example
 * var cov = {
 *   type: 'Coverage',
 *   ...
 *   subsetByIndex: constraints => CovUtils.subsetByIndex(cov, constraints)
 * }
 */
export function subsetByIndex (cov, constraints) {
  return cov.loadDomain().then(domain => {
    constraints = normalizeIndexSubsetConstraints(domain, constraints)
    let newdomain = subsetDomainByIndex(domain, constraints)

    // subset ranges (on request)
    let rangeWrapper = range => {
      let newrange = {
        dataType: range.dataType,
        get: obj => {
          // translate subsetted to original indices
          let newobj = {}
          for (let axisName of Object.keys(obj)) {
            let {start, step} = constraints[axisName]
            newobj[axisName] = start + obj[axisName] * step
          }
          return range.get(newobj)
        }
      }
      newrange.shape = new Map()
      for (let axisName of domain.axes.keys()) {
        let size = newdomain.axes.get(axisName).values.length
        newrange.shape.set(axisName, size)
      }
      return newrange
    }

    let loadRange = key => cov.loadRange(key).then(rangeWrapper)

    let loadRanges = keys => cov.loadRanges(keys).then(ranges => new Map([...ranges].map(([key, range]) => [key, rangeWrapper(range)]))
    )

    // assemble everything to a new coverage
    let newcov = {
      type: COVERAGE,
      domainType: cov.domainType,
      parameters: cov.parameters,
      loadDomain: () => Promise.resolve(newdomain),
      loadRange,
      loadRanges
    }
    newcov.subsetByIndex = subsetByIndex.bind(null, newcov)
    newcov.subsetByValue = subsetByValue.bind(null, newcov)
    return newcov
  })
}

/**
 * Generic subsetByValue function that can be used when building new Coverage objects.
 * Requires cov.subsetByIndex function.
 *
 * @example
 * var cov = {
 *   type: 'Coverage',
 *   ...
 *   subsetByValue: constraints => CovUtils.subsetByValue(cov, constraints)
 * }
 */
export function subsetByValue (cov, constraints) {
  return cov.loadDomain().then(domain => {
    // calculate indices and use subsetByIndex
    let indexConstraints = {}

    for (let axisName of Object.keys(constraints)) {
      let spec = constraints[axisName]
      if (spec === undefined || spec === null || !domain.axes.has(axisName)) {
        continue
      }
      let axis = domain.axes.get(axisName)
      let vals = axis.values

      // special-case handling
      let isISODate = isISODateAxis(domain, axisName)
      let isLongitude = isLongitudeAxis(domain, axisName)

      // wrap input longitudes into longitude range of domain axis
      let lonWrapper = isLongitude ? getLongitudeWrapper(domain, axisName) : undefined

      if (typeof spec === 'number' || typeof spec === 'string' || spec instanceof Date) {
        let match = spec
        if (isISODate) {
          // convert times to numbers before searching
          match = asTime(match)
          vals = vals.map(v => new Date(v).getTime())
        } else if (isLongitude) {
          match = lonWrapper(match)
        }
        let i
        // older browsers don't have TypedArray.prototype.indexOf
        if (vals.indexOf) {
          i = vals.indexOf(match)
        } else {
          i = Array.prototype.indexOf.call(vals, match)
        }
        if (i === -1) {
          throw new Error('Domain value not found: ' + spec)
        }
        indexConstraints[axisName] = i
      } else if ('target' in spec) {
        // find index of value closest to target
        let target = spec.target
        if (isISODate) {
          // convert times to numbers before searching
          target = asTime(target)
          vals = vals.map(v => new Date(v).getTime())
        } else if (isLongitude) {
          target = lonWrapper(target)
        } else if (typeof vals[0] !== 'number' || typeof target !== 'number') {
          throw new Error('Invalid axis or constraint value type')
        }
        let i = indexOfNearest(vals, target)
        indexConstraints[axisName] = i
      } else if ('start' in spec && 'stop' in spec) {
        // TODO what about bounds?

        let {start, stop} = spec
        if (isISODate) {
          // convert times to numbers before searching
          [start, stop] = [asTime(start), asTime(stop)]
          vals = vals.map(v => new Date(v).getTime())
        } else if (isLongitude) {
          [start, stop] = [lonWrapper(start), lonWrapper(stop)]
        } else if (typeof vals[0] !== 'number' || typeof start !== 'number') {
          throw new Error('Invalid axis or constraint value type')
        }

        let [lo1, hi1] = indicesOfNearest(vals, start)
        let [lo2, hi2] = indicesOfNearest(vals, stop)

        // cov is a bit arbitrary and may include one or two indices too much
        // (but since we don't handle bounds it doesn't matter that much)
        let imin = Math.min(lo1, hi1, lo2, hi2)
        let imax = Math.max(lo1, hi1, lo2, hi2) + 1 // subsetByIndex is exclusive

        indexConstraints[axisName] = {start: imin, stop: imax}
      } else {
        throw new Error('Invalid subset constraints')
      }
    }

    return cov.subsetByIndex(indexConstraints)
  })
}