Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
handle longitudes + snap start/stop to axis values (subsetByValue)
Browse files Browse the repository at this point in the history
Snapping start/stop values to axis values increases the chance of using
a cached request in certain subsetting scenarios.
  • Loading branch information
letmaik committed Mar 30, 2016
1 parent 52959ef commit 074aca2
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 15 deletions.
65 changes: 50 additions & 15 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as API from './api.js'
import * as arrays from './arrays.js'
import {shallowcopy, mergeInto} from './util.js'
import {isISODateAxis, isLongitudeAxis, getLongitudeWrapper} from './referencing.js'

// Note: We currently can't handle Hydra data in non-default graphs due to lack of support in JSON-LD framing.

Expand Down Expand Up @@ -299,6 +300,8 @@ function wrappedSubsetByValue (coverage, wrappedCoverage, api, wrapOptions) {
* A safe start/stop would then be a newly calculated axis value which is in the middle
* of the bounds.
*/

// TODO if API axis-value-subsetting is not supported, try to emulate with API axis-index-subsetting

// we split the subsetting constraints into API-compatible and local ones
let apiConstraints = {
Expand All @@ -310,7 +313,7 @@ function wrappedSubsetByValue (coverage, wrappedCoverage, api, wrapOptions) {
let useApi = false
let constraint = constraints[axis]
let cap = caps[axisMap[axis]]
let isTimeString = axisMap[axis] === 'time'
let isTimeString = isISODateAxis(domain, axis)

if (!cap) {
// leave useApi = false
Expand All @@ -319,8 +322,7 @@ function wrappedSubsetByValue (coverage, wrappedCoverage, api, wrapOptions) {
useApi = true
} else if (cap.start && cap.stop) {
// emulate identity match via start/stop if we find a matching axis value
// FIXME handle longitude wrapping
let idx = getClosestIndex(domain, axis, constraint.target, isTimeString)
let {idx} = getClosestIndex(domain, axis, constraint.target)
let val = domain.axes.get(axis).values[idx]
if (isTimeString) {
if (new Date(val).getTime() === new Date(constraint).getTime()) {
Expand All @@ -337,15 +339,33 @@ function wrappedSubsetByValue (coverage, wrappedCoverage, api, wrapOptions) {
useApi = true
} else if (cap.start && cap.stop) {
// emulate target via start/stop
// FIXME handle longitude wrapping
let idx = getClosestIndex(domain, axis, constraint.target, isTimeString)
let val = domain.axes.get(axis).values[idx]
constraint = {start: val, stop: val}
useApi = true
let {idx,outside} = getClosestIndex(domain, axis, constraint.target)
// if the target is outside the axis extent then the API can't be used (as there is no intersection then)
if (!outside) {
let val = domain.axes.get(axis).values[idx]
constraint = {start: val, stop: val}
useApi = true
}
}
} else {
// start / stop
useApi = cap.start && cap.stop

if (useApi) {
// TODO handle bounds

// snap start/stop to axis values to increase the chance of using a cached request
let [vals, start, stop] = prepareForAxisArraySearch(domain, axis, constraint.start, constraint.stop)
let resStart = getClosestIndexArr(vals, start)
let resStop = getClosestIndexArr(vals, stop)
if (resStart.outside && resStop.outside) {
// if both start and stop are outside the axis extent, then snapping would be wrong (has to be an error)
throw new Error('start or stop must be inside the axis extent')
} else {
let axisVals = domain.axes.get(axis).values
constraint = {start: axisVals[resStart.idx], stop: axisVals[resStop.idx]}
}
}
}

if (useApi) {
Expand Down Expand Up @@ -436,15 +456,30 @@ function toLocalConstraintsIfDependencyMissing (apiConstraints, localConstraints
}
}

function getClosestIndex (domain, axis, val, isTimeString) {
let vals = domain.axes.get(axis).values
if (isTimeString) {
function getClosestIndex (domain, axis, val) {
let [axisVals, searchVal] = prepareForAxisArraySearch(domain, axis, val)
return getClosestIndexArr(axisVals, searchVal)
}

function getClosestIndexArr (vals, val) {
let [lo,hi] = arrays.indicesOfNearest(vals, val)
let idx = Math.abs(val - vals[lo]) <= Math.abs(val - vals[hi]) ? lo : hi
return {idx, outside: lo === hi}
}

function prepareForAxisArraySearch (domain, axis, ...searchVal) {
let axisVals = domain.axes.get(axis).values
searchVal = [...searchVal] // to array
if (isISODateAxis(domain, axis)) {
// convert to unix timestamps as we need numbers
val = new Date(val).getTime()
vals = vals.map(t => new Date(t).getTime())
let toUnix = v => new Date(v).getTime()
searchVal = searchVal.map(toUnix)
axisVals = axisVals.map(toUnix)
} else if (isLongitudeAxis(domain, axis)) {
let lonWrapper = getLongitudeWrapper(domain, axis)
searchVal = searchVal.map(lonWrapper)
}
let idx = arrays.indexOfNearest(vals, val)
return idx
return [axisVals, ...searchVal]
}

/**
Expand Down
111 changes: 111 additions & 0 deletions src/referencing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// COPIED FROM covjson-reader -> DRY!!!!!

const OPENGIS_CRS_PREFIX = 'http://www.opengis.net/def/crs/'

/** 3D WGS84 in lat-lon-height order */
const EPSG4979 = OPENGIS_CRS_PREFIX + '/EPSG/0/4979'

/** 2D WGS84 in lat-lon order */
const EPSG4326 = OPENGIS_CRS_PREFIX + '/EPSG/0/4326'

/** 2D WGS84 in lon-lat order */
const CRS84 = OPENGIS_CRS_PREFIX + '/OGC/1.3/CRS84'

/** CRSs in which position is specified by geodetic latitude and longitude */
const EllipsoidalCRSs = [EPSG4979, EPSG4326, CRS84]

/** Position of longitude axis */
const LongitudeAxisIndex = {
[EPSG4979]: 1,
[EPSG4326]: 1,
[CRS84]: 0
}

/**
* Returns a function which converts an arbitrary longitude to the
* longitude extent used in the coverage domain.
* This only supports primitive axes since this is what subsetByValue supports.
* The longitude extent is extended to 360 degrees if the actual extent is smaller.
* The extension is done equally on both sides of the extent.
*
* For example, the domain may have longitudes within [0,360].
* An input longitude of -70 is converted to 290.
* All longitudes within [0,360] are returned unchanged.
*
* If the domain has longitudes within [10,50] then the
* extended longitude range is [-150,210] (-+180 from the middle point).
* An input longitude of -170 is converted to 190.
* All longitudes within [-150,210] are returned unchanged.
*
* @ignore
*/
export function getLongitudeWrapper (domain, axisName) {
// for primitive axes, the axis identifier = component identifier
if (!isLongitudeAxis(domain, axisName)) {
throw new Error(`'${axisName}' is not a longitude axis`)
}

let vals = domain.axes.get(axisName).values
let lon_min = vals[0]
let lon_max = vals[vals.length-1]
if (lon_min > lon_max) {
[lon_min,lon_max] = [lon_max,lon_min]
}

let x_mid = (lon_max + lon_min) / 2
let x_min = x_mid - 180
let x_max = x_mid + 180

return lon => {
if (x_min <= lon && lon <= x_max) {
// directly return to avoid introducing rounding errors
return lon
} else {
return ((lon - x_min) % 360 + 360) % 360 + x_min
}
}
}

/**
* Return whether the given domain axis represents longitudes.
*
* @ignore
*/
export function isLongitudeAxis (domain, axisName) {
let ref = getReferenceObject(domain, [axisName])
if (!ref) {
return false
}

let crsId = ref.system.id
// TODO should support unknown CRSs with embedded axis information
if (EllipsoidalCRSs.indexOf(crsId) === -1) {
// this also covers the case when there is no ID property
return false
}

let compIdx = ref.components.indexOf(axisName)
let isLongitude = LongitudeAxisIndex[crsId] === compIdx
return isLongitude
}

/**
* Returns true if the given axis has ISO8601 date strings
* as axis values.
*/
export function isISODateAxis (domain, axisName) {
let val = domain.axes.get(axisName).values[0]
if (typeof val !== 'string') {
return false
}
return !isNaN(new Date(val).getTime())
}

/**
* Return the reference system connection object for the given domain component,
* or undefined if none exists.
*/
function getReferenceObject (domain, component) {
let ref = domain.referencing.find(ref => ref.components.indexOf(component) !== -1)
return ref
}

0 comments on commit 074aca2

Please sign in to comment.