UNPKG

dasf-web

Version:

Web frontend components for the data analytics software framework (DASF)

1,470 lines (1,232 loc) 48.5 kB
import { Extent, createEmpty, intersects } from 'ol/extent'; import ImageSource from 'ol/source/Image'; import ImageBase from 'ol/ImageBase'; import ImageState from 'ol/ImageState'; import { fromLonLat, transformExtent, ProjectionLike } from 'ol/proj.js'; import axios from 'axios'; import netcdfjs from 'netcdfjs'; import DateMap from '../../util/DateMap'; import NumericalParameter from './Parameter'; import Gradient, { Spectral } from '../../colors/Gradient'; import TemporalData from './TemporalData'; import DateRange from '../../dateSelection/daterangepicker/DateRange'; import Filter from './Filter'; import IHasFilter from './HasFilter'; import FilterThresholdCalculator from './FilterThresholdCalculator'; import HasParameterGradient from './HasParameterGradient'; import EpsgIO from '../utils/EpsgIO'; import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; import { ICancel } from 'typescript-observable/dist/interfaces/cancel'; import MathUtils from '../../util/MathUtils'; import ArrayUtils from '../../util/ArrayUtils'; import Histogram from '../../model/Histogram'; import b64a from 'base64-arraybuffer'; import VectorSource from 'ol/source/Vector'; import Geometry from 'ol/geom/Geometry'; import Point from 'ol/geom/Point'; import { Feature } from 'ol'; import MultiPoint from 'ol/geom/MultiPoint'; import MultiPolygon from 'ol/geom/MultiPolygon'; import Polygon from 'ol/geom/Polygon'; export type Raster = number[]; export class Coverage { public data: Raster; public param: NumericalParameter; public readonly date: Date; public readonly width: number; public readonly height: number; public readonly invertYAxis: boolean; public constructor(data: Raster, param: NumericalParameter, date: Date, width: number, height: number, invertYAxis: boolean) { this.data = data; this.param = param; this.date = date; this.width = width; this.height = height; this.invertYAxis = invertYAxis; } public getImage(gradient?: Gradient, filter?: Filter): HTMLCanvasElement { if (!gradient) { // no gradient given - create a temporary one gradient = this.createDefaultGradient(); } return this.renderImage(gradient, filter); } private createDefaultGradient(): Gradient { let min: number = (this.param.hasMin() ? this.param.getMin() : MathUtils.min(this.data)) as number; let max: number = (this.param.hasMax() ? this.param.getMax() : MathUtils.max(this.data)) as number; // do we have a fill value? if (!this.param.hasFill()) { // no fill value - guess one if (min <= -2147483648) { // use min as no data value // replace all min values with NaN and find new min MathUtils.replace(min, Number.NaN, this.data); min = MathUtils.min(this.data); this.param.setFill(Number.NaN); } } if (!this.param.hasMin()) { this.param.setMin(min); } if (!this.param.hasMax()) { this.param.setMax(max); } return new Gradient(Spectral, min, max); } private renderImage(gradient: Gradient, filter?: Filter): HTMLCanvasElement { // initialize canvas with detected dimensions let canvas: HTMLCanvasElement = document.createElement('canvas'); canvas.width = this.width; canvas.height = this.height; // get the rendering context to put the pixel values from the data set variables let ctx: CanvasRenderingContext2D | null = canvas.getContext('2d'); if (ctx) { // diable antialising ctx.imageSmoothingEnabled = false; // init image and pixel array let image: ImageData = ctx.createImageData(this.width, this.height); let pixels: Uint8ClampedArray = image.data; let noDataValue = this.param.getFill(); let hasNoData = this.param.hasFill(); // put the raster data into the image pixels for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { let dataIdx = y * this.width + x; let value = this.data[dataIdx]; if (Number.isNaN(value) || (hasNoData && value === noDataValue) || (filter != null && !filter.filter(value))) { // no data - skip // let pix = [0, 0, 0, 255]; // pixels.set(pix, idx * 4); continue; } let pixelIdx = dataIdx; if (this.invertYAxis) { pixelIdx = ((this.height - 1 - y) * this.width + x); } // get color from gradient and put pixel values let pix = gradient.getColorRGBA(value); pixels.set(pix, pixelIdx * 4); } } // write the image into the canvas context ctx.putImageData(image, 0, 0); } else { console.error( 'unable to create canvas rendering context' ); } return canvas; } public extractExtent ( extent: Extent, filter?: Filter ): number[] { const values: number[] = [] let noDataValue = this.param.getFill(); let hasNoData = this.param.hasFill(); // put the raster data into the image pixels for (let y = extent[1]; y < extent[3]; ++y) { for (let x = extent[0]; x < extent[2]; ++x) { let dataIdx = y * this.width + x; let value = this.data[dataIdx]; if (Number.isNaN(value) || (hasNoData && value === noDataValue) || (filter != null && !filter.filter(value))) { // no data - skip continue; } values.push(value) } } return values } public getHistogram(numBuckets: number, filter: Filter, sampleBounds: [number, number] = [NaN, NaN]): Histogram { let min = Number.NaN; if (!Number.isNaN(sampleBounds[0])) { min = sampleBounds[0]; } else if (filter) { if (this.param.hasMin()) { // there is a filter and a known parameter minimum min = Math.max(filter.threshold[0], this.param.getMin()); } else { min = filter.threshold[0]; } } else if (this.param.hasMin()) { // no filter, but parameter min min = this.param.getMin(); } if (Number.isNaN(min)) { // unknown min - determine min = MathUtils.min(this.data); } let max = Number.NaN; if (!Number.isNaN(sampleBounds[1])) { max = sampleBounds[1]; } else if (filter) { if (this.param.hasMax()) { // there is a filter and a known parameter maximum max = Math.min(filter.threshold[1], this.param.getMax()); } else { max = filter.max; } } else if (this.param.hasMax()) { // no filter, but parameter min max = this.param.getMax(); } if (Number.isNaN(max)) { // unknown max - determine max = MathUtils.max(this.data); } return ArrayUtils.histogram(this.data, numBuckets, this.param.getFill(), min, max); } public toJson(filter?: Filter): object { let dataObject: object = {}; dataObject["dimension"] = { width: this.width, height: this.height, inverted: this.invertYAxis }; dataObject["date"] = this.date.toISOString(); let data = this.data; if (filter) { // apply filter before encoding // set nodata let noData = this.param.hasFill() ? this.param.getFill() : Number.NaN; // clone array data = Object.assign([], this.data); // set filtered values to noData for (let i = 0; i < data.length; ++i) { let value = data[i]; if (Number.isNaN(value) || value == noData) { // the value already is noData - ignore continue; } else if (!filter.filter(value)) { // this value does not satisfy the filter - set to noData data[i] = noData; } } } dataObject["data"] = b64a.encode(new Float32Array(data).buffer); return dataObject; } } class RenderedConverage extends ImageBase { private _canvas: HTMLCanvasElement | undefined; public constructor(extent, resolution) { super(extent, resolution, 1, ImageState.LOADED); } public getImage(): HTMLCanvasElement | undefined { return this._canvas; } public renderCoverage(coverage: Coverage, gradient: Gradient | undefined, filter?: Filter): void { // we need to sqeeze the coverage into its defined extent // therefore we 'draw' the coverage image into a new canvas having the same ratio as the extent let extent: Extent = this.getExtent(); let extentRatio = (extent[2] - extent[0]) / (extent[3] - extent[1]); let canvasWidth = coverage.height * extentRatio; let canvasHeight = coverage.height; let renderedCanvas: HTMLCanvasElement = document.createElement('canvas'); renderedCanvas.width = canvasWidth renderedCanvas.height = canvasHeight; let ctx: CanvasRenderingContext2D | null = renderedCanvas.getContext('2d'); if (ctx) { // diable antialising ctx.imageSmoothingEnabled = false; ctx.drawImage(coverage.getImage(gradient, filter), 0, 0, canvasWidth, canvasHeight); } this._canvas = renderedCanvas; } public load(): void { // nothing to do here } } export default class NetcdfRasterSource extends ImageSource implements TemporalData, IHasFilter, HasParameterGradient { public static readonly DATA_PROP = 'data'; public static readonly PARAMETER_PROP = 'parameter'; private dateCoverageMap: DateMap<Map<string, Coverage>>; private activeDate: Date; private activeParameter: string; private dateRange: DateRange; private parameters: NumericalParameter[]; private gradient: Gradient; private filter?: Filter; private extent: Extent; private resolution = 0; private coverageExtent: Extent; private projection: ProjectionLike; private static VALID_MAX = (Math.pow(2, 31) - 1); /** * internal rendered image */ private _image: RenderedConverage; // TODO: add means to alter the shown date, paramerter and the used colorscale public constructor(staticSourceOptions: object) { super(staticSourceOptions); this.dateCoverageMap = staticSourceOptions['coverages']; this.activeDate = staticSourceOptions['date'] as Date; this.activeParameter = staticSourceOptions['parameter'] as string; this.parameters = staticSourceOptions['availableParameters']; this.dateRange = new DateRange(...this.dateCoverageMap.dateRange()); this.extent = staticSourceOptions['imageExtent']; this.coverageExtent = staticSourceOptions['coverageExtent']; if (!this.coverageExtent) { // missing coverageExtent - use imageExtent as fallback this.coverageExtent = this.extent; } this.resolution = staticSourceOptions['resolution']; this.projection = staticSourceOptions['projection']; this._image = new RenderedConverage(this.coverageExtent, this.resolution); this.bootstrapFilter(); this.update(); } public static async create(options: object): Promise<NetcdfRasterSource> { return new NetcdfRasterSource(await NetcdfRasterSource.toStaticSourceOptions(options)); } public static createFromArray(options: object): NetcdfRasterSource { return new NetcdfRasterSource(NetcdfRasterSource.loadFromArray(options)); } private static async toStaticSourceOptions(options: object): Promise<object> { if (options.hasOwnProperty(NetcdfRasterSource.DATA_PROP)) { let data: object = options[NetcdfRasterSource.DATA_PROP]; if (typeof data === 'string') { return NetcdfRasterSource.loadFromUrl(options); } else if (data instanceof ArrayBuffer) { return NetcdfRasterSource.loadFromBuffer(options); } else if (data instanceof Blob) { // (data as Blob).arrayBuffer() return NetcdfRasterSource.loadFromBlob(options); } else if (data instanceof Array) { return NetcdfRasterSource.loadFromArray(options); } else { console.warn('unsupported data option'); console.warn(options); } } else { console.warn('missing mandatory data property in options'); } return {}; } private static loadFromArray(options: object): object { // check neseccary options if (!options.hasOwnProperty(NetcdfRasterSource.DATA_PROP)) { console.warn('missing :data: option property for array2raster_source'); return {}; } if (!options.hasOwnProperty(NetcdfRasterSource.PARAMETER_PROP)) { console.warn('missing :parameter: option property for array2raster_source'); return {}; } // extract options parameters let data: Raster = options[NetcdfRasterSource.DATA_PROP]; let parameter: NumericalParameter = options[NetcdfRasterSource.PARAMETER_PROP]; let date: Date = options.hasOwnProperty('date') ? options['date'] : new Date(); let extent: Extent = options['imageExtent']; let imgSize: number[] = options['imageSize']; let invertYAxis: boolean = options.hasOwnProperty('invertYAxis') ? options['invertYAxis'] : false; // initialize coverage let coverage: Coverage = new Coverage(data, parameter, date, imgSize[0], imgSize[1], invertYAxis); // initialize parameter map let parameterMap: Map<string, Coverage> = new Map<string, Coverage>(); parameterMap.set(parameter.getName(), coverage); // initialize date coverage map let dateCoverageMap: DateMap<Map<string, Coverage>> = new DateMap<Map<string, Coverage>>(); dateCoverageMap.set(date, parameterMap); // determine resolution let yResolution = (extent[3] - extent[1]) / imgSize[1]; return { coverages: dateCoverageMap, date: date, parameter: parameter.getName(), availableParameters: [parameter], // [left, bottom, right, top] imageExtent: extent, resolution: yResolution, projection: 'EPSG:3857', imageSmoothing: false }; } private static async loadFromUrl(options: object): Promise<object> { // TODO: load from url via axios, then load buffered netcdf const response = await axios.get(options[NetcdfRasterSource.DATA_PROP], { responseType: 'arraybuffer' }); options[NetcdfRasterSource.DATA_PROP] = response.data; return NetcdfRasterSource.loadFromBuffer(options); } private static async loadFromBlob(options: object): Promise<object> { let buffer = await new Response(options[NetcdfRasterSource.DATA_PROP]).arrayBuffer(); if (buffer) { options[NetcdfRasterSource.DATA_PROP] = buffer; return NetcdfRasterSource.loadFromBuffer(options); } else { console.warn('unable to read blob/file.'); return {}; } } private static async loadFromBuffer(options: object): Promise<object> { let reader: netcdfjs = new netcdfjs(options[NetcdfRasterSource.DATA_PROP]); // build the date coverage map let coverageBuild: object = NetcdfRasterSource.buildDateCoverageMap(reader); if (coverageBuild == undefined) { throw new Error('error building coverages from netcdf data'); } let dateCoverageMap: DateMap<Map<string, Coverage>> = coverageBuild['map']; let parameters: NumericalParameter[] = coverageBuild['parameters']; if (!dateCoverageMap) { // no or unsupported data found return {}; } let extent = await NetcdfRasterSource.extractExtent(reader); if (extent.length < 4) { // invalid extent return {}; } let firstEntry = dateCoverageMap.entries().next().value; let firstParam = firstEntry[1].entries().next().value; let firstCoverage: Coverage = firstParam[1]; let crs: string = 'crs' in extent ? extent['crs'] : 'EPSG:3857'; let crsExtent: Extent = extent['crsExtent']; // calculate resoultion as the ratio of real world extent and image size let realworldHeight = extent[3] - extent[1]; let yResolution = (realworldHeight / firstCoverage.height); return { coverages: dateCoverageMap, date: firstEntry[0], parameter: firstParam[0], availableParameters: parameters, // [left, bottom, right, top] imageExtent: extent, coverageExtent: crsExtent, resolution: yResolution, projection: crs, imageSmoothing: false }; } private static async extractExtent(reader: netcdfjs): Promise<Extent> { console.log('extracting extent...'); // first: check for 'geospatial min/max values' var latMin = reader.getAttribute('geospatial_lat_min'); var latMax = reader.getAttribute('geospatial_lat_max'); var lonMin = reader.getAttribute('geospatial_lon_min'); var lonMax = reader.getAttribute('geospatial_lon_max'); if (latMin !== null && latMax !== null && lonMin !== null && lonMax !== null) { // convert to map projection let minPos = fromLonLat([lonMin, latMin]); let maxPos = fromLonLat([lonMax, latMax]); return [minPos[0], minPos[1], maxPos[0], maxPos[1]]; } // second: extract min/max values from lat/lon dim variables let lats: number[] = []; let lons: number[] = []; if (reader.dataVariableExists('lat')) { lats = reader.getDataVariable('lat'); } else if (reader.dataVariableExists('latitude')) { lats = reader.getDataVariable('latitude'); } else if (reader.dataVariableExists('Lat')) { lats = reader.getDataVariable('Lat'); } if (reader.dataVariableExists('lon')) { lons = reader.getDataVariable('lon'); } else if (reader.dataVariableExists('longitude')) { lons = reader.getDataVariable('longitude'); } else if (reader.dataVariableExists('Long')) { lons = reader.getDataVariable('Long'); } else if (reader.dataVariableExists('Lon')) { lons = reader.getDataVariable('Lon'); } if (lats.length > 0 && lons.length > 0) { latMin = MathUtils.min(lats); latMax = MathUtils.max(lats); lonMin = MathUtils.min(lons); lonMax = MathUtils.max(lons); if (latMin !== undefined && latMax !== undefined && lonMin !== undefined && lonMax !== undefined) { // convert to map projection let minPos = fromLonLat([lonMin, latMin]); let maxPos = fromLonLat([lonMax, latMax]); // FIXME: deal with a raster that has only one pixel and therefore identical min/max for lat and lon // as a workaround we render a pixel roughly one degree in size, this size should be extracted from the meta data. const delta = 111100 / 2; if (minPos[0] == maxPos[0]) { minPos[0] -= delta maxPos[0] += delta } if (minPos[1] == maxPos[1]) { minPos[1] -= delta maxPos[1] += delta } console.log(minPos, maxPos); return [minPos[0], minPos[1], maxPos[0], maxPos[1]]; } else { console.warn('unable to extract spatial bounds from netcdf'); return createEmpty(); } } // third check x/y and crs // check crs first let crs: string = NetcdfRasterSource.findCrsAttributeValue(reader.globalAttributes); if (crs === undefined || crs.length == 0) { console.warn('unable to extract spatial bounds from netcdf'); return createEmpty(); } let proj4def: string = ''; if (crs.startsWith('+')) { // found proj4 def - use it right away proj4def = crs; } else { if(!isNaN(+crs)) { // number only - prepend epsg crs = 'epsg:'+ crs; } // propably epsg string - query EpsgIO for proj4 definition crs = crs.toUpperCase(); proj4def = await EpsgIO.search(crs); } if (!proj4def || proj4def.length == 0) { throw new Error('unable to find projection parameters for crs: ' + crs); } // register proj4 definition proj4.defs(crs, proj4def); register(proj4); // we have a src crs let x: number[] = []; let y: number[] = []; if (reader.dataVariableExists('x')) { x = reader.getDataVariable('x') as number[]; } if (reader.dataVariableExists('y')) { y = reader.getDataVariable('y') as number[]; } // setUserProjection(crs); // let min = fromUserCoordinate([x[0], y[0]]); // let max = fromUserCoordinate([x[x.length-1], y[y.length-1]], crs); // clearUserProjection(); // extent in view projection (epsg:3857) // minX = 1525338.53 // minY = 6569752.93 // maxX = 1547490.80 // maxY = 6617676.68 // let extent: Extent = [min[0], min[1], max[0], max[1]]; let crsExtent: Extent = [x[0], y[0], x[x.length - 1], y[y.length - 1]]; if (crsExtent[3] < crsExtent[1]) { // inverted y let temp = crsExtent[1]; crsExtent[1] = crsExtent[3]; crsExtent[3] = temp; } let extent: Extent; if (crs.toUpperCase() === 'EPSG:3857') { extent = crsExtent; // ensure we have square pixels const yRes = (extent[3] - extent[1]) / y.length; const ratioDif = 0.5 * ((extent[2] - extent[0]) - (yRes * x.length)) extent[0] += ratioDif; extent[2] -= ratioDif; // for the corner pixels to cover the extent locations, we have to move it by half a pixel each extent[0] -= yRes/2; extent[2] += yRes/2; extent[1] -= yRes/2 * y.length / x.length; // keep aspect ratio extent[3] += yRes/2 * y.length / x.length; // keep aspect ratio } else { extent = transformExtent(crsExtent, crs, 'EPSG:3857') } // console.log('layer extent in 4326', transformExtent(crsExtent, crs, 'EPSG:4326')); // let extent: Extent = [1525338.53, 6569752.93, 1547490.80, 6617676.68]; extent['crs'] = crs; extent['crsExtent'] = crsExtent; return extent; // console.warn('unable to extract spatial bounds from netcdf'); // return createEmpty(); } private static buildDateCoverageMap(reader: netcdfjs): object | undefined { let map: DateMap<Map<string, Coverage>> = new DateMap<Map<string, Coverage>>(); // get the defined dimensions let dimensions: object = reader['dimensions']; if (!dimensions) { console.warn( 'dataset does not provide <dimension> information - ignoring it' ); return undefined; } // get lat/lon dimensions let width = 0; let height = 0; let timeDim = 0; let timeVarName = 'time'; let xVarName = 'lon'; let yVarName = 'lat'; let projectedDataset = false; for (let dim of dimensions as object[]) { let name: string = dim['name']; let size: number = dim['size']; switch (name) { case 'x': case 'easting': projectedDataset = true; width = size; xVarName = name; break; case 'lon': case 'long': case 'longitude': case 'Lon': case 'Long': projectedDataset = false; width = size; xVarName = name; break; case 'y': case 'northing': projectedDataset = true; height = size; yVarName = name; break; case 'lat': case 'latitude': case 'Lat': case 'Latitude': projectedDataset = false; height = size; yVarName = name; break; case 'time': case 'ansi': timeDim = size; timeVarName = name; break; default: console.info("ignoring unknown dimension '" + name + "'"); break; } } if (width == 0 || height == 0) { console.warn( 'unable to detect lat/lon dimension size - ignoring data set' ); return undefined; } let times: number[] = [0]; if (timeDim || reader.dataVariableExists(timeVarName)) { times = reader.getDataVariable(timeVarName); timeDim = times.length; } let parameters: NumericalParameter[] = NetcdfRasterSource.getParameters(reader); let numValues = width * height; let timeUnitScale: number[] = NetcdfRasterSource.extractTimeUnitScale(reader, timeVarName); let invertYAxis: boolean = NetcdfRasterSource.isYAxisInverted(reader, yVarName); // check before hand, if there are global min/max values provided by the netcdf head let hasGlobalRange: Set<NumericalParameter> = new Set(); for (let param of parameters) { if (param.hasMin() && param.hasMax()) { hasGlobalRange.add(param); } } for (let t = 0; t < times.length; ++t) { // decode date, by converting unix seconds to milliseconds let date: Date = new Date(timeUnitScale[0] + times[t] * timeUnitScale[1]); if (timeVarName == 'ansi' && Number.isInteger(times[t])) { date.setHours(0, 0, 0, 0); } // initialize the date map value let dateMap: Map<string, Coverage> = new Map(); map.set(date, dateMap); // extract all coverages for this date for (let param of parameters) { // get the data array for this parameter let offset = t * numValues; let data = reader.getDataVariable(param.getName()); let raster: Raster = null; let renderOption: string = ""; for (let dimPos of param.getDimensions()) { let dimName: string = dimensions[dimPos]['name']; if (dimName == timeVarName) { dimName = 'time'; } else if (dimName == xVarName) { dimName = 'lon'; } else if (dimName == yVarName) { dimName = 'lat'; } else { console.warn('ignoring unknown dimension for rendering', dimName); } renderOption = renderOption + dimName; } if (data.length === timeDim) { // this is an array of arrays - select the one for the given time t raster = data[t]; } else { switch (renderOption) { case 'latlontime': raster = new Array<number>(numValues); for (let i = 0; i < numValues; ++i) { raster[i] = data[i * times.length + t]; } break; case 'timelatlon': raster = data.slice(offset, offset + numValues); break; default: raster = data.slice(offset, offset + numValues); break; } } if (!Array.isArray(raster)) { raster = [raster]; } // update min max ranges if missing if (!hasGlobalRange.has(param)) { let min = MathUtils.min(raster, param.getFill()); if (min <= -NetcdfRasterSource.VALID_MAX) { // propably MIN_INT fill value - replace param.setFill(Number.NaN) MathUtils.replace(min, Number.NaN, raster); min = MathUtils.min(raster); } if (Number.isFinite(min)) { if (!param.hasMin() || min < param.getMin()) { param.setMin(min) } } } if (!hasGlobalRange.has(param)) { let max = MathUtils.max(raster); if (Number.isFinite(max)) { if (!param.hasMax() || max > param.getMax()) { param.setMax(max) } } } dateMap.set(param.getName(), new Coverage(raster, param, date, width, height, invertYAxis)); } } return { 'map': map, 'parameters': parameters }; } private static isYAxisInverted(reader: netcdfjs, yVarName: string): boolean { let lats = []; if (reader.dataVariableExists(yVarName)) { lats = reader.getDataVariable(yVarName); } if (lats.length == 0) { console.warn('unable to find latitude variable, assuming default y axis orientation'); return false; } // we have an inverted axis if the latitude values are in ascending order return lats[0] < lats[lats.length - 1]; } private static extractTimeUnitScale(reader: netcdfjs, timeVarName: string): number[] { if (timeVarName == 'ansi') { // ansi time is days since 1601 let offset = new Date('12/31/1600').getTime(); let scale = 1000 * 60 * 60 * 24; return [offset, scale]; } if ('header' in reader && 'variables' in reader['header']) { // find 'time' variable for (let variable of reader['header']['variables']) { if ('name' in variable && variable['name'] === timeVarName && 'attributes' in variable) { // find units attribute for (let attribute of variable['attributes']) { if ('name' in attribute && attribute['name'] === 'units' && 'value' in attribute) { // found time units attribute - parse it let value: string = attribute['value']; let split: string[] = value.split(' since '); let date: Date = new Date(split[1]); let offset = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()); let scale = 1000; switch (split[0]) { case 'months': scale = 1000 * 60 * 60 * 24 * 29; case 'days': scale = 1000 * 60 * 60 * 24; break; case 'hours': scale = 1000 * 60 * 60; break; case 'minutes': scale = 1000 * 60; break; case 'seconds': scale = 1000; break; default: console.warn('unable to parse time scale from units - assuming senconds'); scale = 1000; break; } return [offset, scale]; } } } } } console.warn('unable to extract time unit definition, assuming seconds since 1970') return [0, 1000]; } private static getParameters(reader: netcdfjs): NumericalParameter[] { let parameters: NumericalParameter[] = []; for (let parameter of reader['header']['variables']) { if (parameter['dimensions'] && parameter['dimensions']['length'] < 2) { // this is no spatial parameter, maybe one of the axis like lat/lon/time - skip continue; } let name: string = parameter['name']; if (name === 'time' || name === 'ansi' || name === 'lat' || name === 'lon' || name === 'latitude' || name === 'longitude' || name === 'easting' || name === 'northing' || name.endsWith('_bnds')) { // axis parameter - skip continue; } let min: number | undefined = NetcdfRasterSource.findMinAttributeValue(parameter); let max: number | undefined = NetcdfRasterSource.findMaxAttributeValue(parameter); let fill: number | undefined = NetcdfRasterSource.findFillAttributeValue(parameter); let dimensions: number[] = parameter['dimensions']; parameters.push(new NumericalParameter(name, min, max, fill, dimensions)) } return parameters; } private static findMinAttributeValue(parameter: object): number | undefined { // check for 'min' let min: string | number | undefined = NetcdfRasterSource.findAttributeValue(parameter, 'min'); if (min !== undefined) { // found return Number(min); } // not found, check for 'valid_min' min = NetcdfRasterSource.findAttributeValue(parameter, 'valid_min'); if (min !== undefined) { if (Number(min) < -NetcdfRasterSource.VALID_MAX) { // unlikely return undefined; } // found return Number(min); } // not found at all return undefined; } private static findMaxAttributeValue(parameter: object): number | undefined { // check for 'max' let max: string | number | undefined = NetcdfRasterSource.findAttributeValue(parameter, 'max'); if (max !== undefined) { // found return Number(max); } // not found, check for 'valid_max' max = NetcdfRasterSource.findAttributeValue(parameter, 'valid_max'); if (max !== undefined) { if (Number(max) > NetcdfRasterSource.VALID_MAX) { // unlikely return undefined; } // found return Number(max); } // not found at all return undefined; } private static findFillAttributeValue(parameter: object): number | undefined { // check for '_FillValue' let fill: string | number | undefined = NetcdfRasterSource.findAttributeValue(parameter, '_FillValue'); if (fill !== undefined) { // found return Number(fill); } // not found, check for 'missing_value' fill = NetcdfRasterSource.findAttributeValue(parameter, 'missing_value'); if (fill !== undefined) { // found return Number(fill); } // not found, check for '_NoData' fill = NetcdfRasterSource.findAttributeValue(parameter, '_NoData'); if (fill !== undefined) { // found return Number(fill); } // not found, check for 'nodata_value' fill = NetcdfRasterSource.findAttributeValue(parameter, 'nodata_value'); if (fill !== undefined) { // found return Number(fill); } // not found at all return undefined; } private static findAttributeValue(parameter: object, attributeName: string): string | number | undefined { for (let attr of parameter['attributes']) { if (attr['name'] === attributeName) { return attr['value']; } } return undefined; } private static findCrsAttributeValue(globalAttributes: object[]): string { for (let attr of globalAttributes) { if (attr['name'].toString().toUpperCase() === 'CRS') { return attr['value']; } } return undefined; } public splitToSingleParameterSources(): NetcdfRasterSource[] { let sources: NetcdfRasterSource[] = []; for (let parameter of this.parameters) { sources.push(this.cloneParameterSource(parameter)); } return sources; } public cloneParameterSource(parameter: NumericalParameter): NetcdfRasterSource { // clone coverageMap let newCovMap = new DateMap<Map<string, Coverage>>(); for (let date of this.dateCoverageMap.keys()) { let paraMap = this.dateCoverageMap.get(date); if (paraMap && paraMap.has(parameter.getName())) { let newParaMap = new Map<string, Coverage>(); newParaMap.set(parameter.getName(), paraMap.get(parameter.getName())); newCovMap.set(date, newParaMap); } } let firstEntry = newCovMap.entries().next().value; let firstParam = firstEntry[1].entries().next().value; let opts = { coverages: newCovMap, date: firstEntry[0], parameter: firstParam[0], availableParameters: [parameter], imageExtent: this.extent, coverageExtent: this.coverageExtent, resolution: this.resolution, projection: this.projection } return new NetcdfRasterSource(opts); } /** * Vectorizes this raster source. A vector source for the active date is generated. * * @param geometryType * resulting geometry, possible values are: * - 'pixel-points': single point geometry per raster pixel, * - 'pixel-squares': single squares per raster pixel (only works with 'epsg:3857' as raster crs) * @param groupingProperty * pixel geometries are grouped into MultiGeometry features, * there will be as many features as there are distinct property values. * This discards all properties except for the given grouping property. * @returns */ public vectorize(geometryType: string = 'pixel-points', groupingByParameterName?: string, targetCrs: ProjectionLike = 'epsg:3857'): VectorSource { if (groupingByParameterName) { // check if we have this parameter const param = this.getAvailableParameters().find((p: NumericalParameter) => p.getName() === groupingByParameterName) if (param === undefined) { throw new Error('unknown grouping parameter name'); } } // create geometries get a coverage first const cov: Coverage = this.getCoverage() const e: Extent = this.extent; const width = cov.width; const height = cov.height; const xDif = (e[2] - e[0]) / width; const yDif = (e[3] - e[1]) / height; const xOff = 0.5 * xDif; const yOff = 0.5 * yDif; const geometries: Geometry[] = []; for (let rasterY = 0; rasterY < height; ++rasterY) { const geometryY = e[1] + rasterY * yDif; for (let rasterX = 0; rasterX < width; ++rasterX) { const geometryX = e[0] + rasterX * xDif; // create geometry switch (geometryType) { case 'pixel-points': geometries.push(new Point([geometryX + xOff, geometryY + yOff])) break; case 'pixel-squares': geometries.push(new Polygon([[[geometryX, geometryY], [geometryX + xDif, geometryY], [geometryX + xDif, geometryY + yDif], [geometryX, geometryY + yDif], [geometryX, geometryY]]])); break; } } } // transform geometries to target projection if (this.projection.toString().localeCompare(targetCrs.toString(), undefined, { sensitivity: 'accent' }) !== 0) { geometries.forEach((geom) => geom.transform(this.projection, targetCrs)); } // convert geometries into features const features: Feature[] = []; if (groupingByParameterName) { const raster: Raster = this.getCoverage(this.activeDate, groupingByParameterName).data; const valueMap: Map<number, Geometry[]> = new Map() for (let y = 0; y < height; ++y) { for (let x = 0; x < width; ++x) { const value: number = raster[y * width + x]; let valueGeometries: Geometry[]; if (valueMap.has(value)) { valueGeometries = valueMap.get(value); } else { valueGeometries = []; valueMap.set(value, valueGeometries); } valueGeometries.push(geometries[y * width + x]); } } // now that all geometries are grouped we can create multi geometry features for each value for (let [key, value] of valueMap.entries()) { let geom: Geometry; switch (geometryType) { case 'pixel-points': geom = new MultiPoint([]); value.forEach((point: Geometry) => (geom as MultiPoint).appendPoint(point as Point)) break; case 'pixel-squares': geom = new MultiPolygon(value as Polygon[]); break; } const feature: Feature = new Feature(geom); feature.set(groupingByParameterName, key); features.push(feature); } } else { // no grouping - iterate over all parameter coverages and append parameter values to each geometry feature // convert geometries to features first geometries.forEach((geom) => features.push(new Feature(geom))) for (let parameter of this.getAvailableParameters()) { const raster: Raster = this.getCoverage(this.activeDate, parameter.getName()).data; for (let y = 0; y < height; ++y) { for (let x = 0; x < width; ++x) { features[y * width + x].set(parameter.getName(), raster[y * width + x]); // features[y*(width+1) + x].set(parameter.getName(), raster[y*width + x]); } } } } return new VectorSource({ features: features, }) } public getExtent(): Extent { return this.extent; } public getDate(): Date { return this.activeDate; } public setDate(date: Date): void { if (date === undefined) { return; } if (this.dateCoverageMap.has(date)) { // this is a valid date we have data for this.activeDate = date; } else { // this is not a valid date we have data for // find closest valid date let minDelta = Number.MAX_VALUE; let closestValidDate: Date = this.activeDate; for (let validDate of this.dateCoverageMap.keys()) { let delta = Math.abs(date.getTime() - validDate.getTime()); if (delta < minDelta) { minDelta = delta; closestValidDate = validDate; } } this.activeDate = closestValidDate; } if (this.filter && !this.filter.useTimeseries) { // the date changed and the filter is bound to single rasters this.bootstrapFilter(); } this.update(); } public getDateRange(): DateRange { return this.dateRange; } public getAllowedDates(): Date[] { return Array.from(this.dateCoverageMap.keys()); } public getAvailableParameters(): NumericalParameter[] { return this.parameters; } public getGradient(): Gradient | undefined { return this.gradient; } private gradientRegistration: ICancel = null; public setGradient(gradient: Gradient): void { this.gradient = gradient; if (this.gradientRegistration) { this.gradientRegistration.cancel(); } this.gradientRegistration = this.gradient.on('changed', () => { // gradient changed - update raster this.update(); }) this.update(); } public getActiveParameter(): NumericalParameter { return this.parameters.find((param) => { return param.getName() == this.activeParameter }); } private bootstrapFilter(): void { // if (this.filter && this.filter.attribute == this.activeParameter) { // return; // } let activeParam = this.getActiveParameter(); if (activeParam == undefined) { return; } // create filter let filter: Filter; if (this.filter) { // we already have a filter - get and update filter = this.filter; } else { // no filter yet - create one filter = new Filter(); } // initialize threshold calculator let filterCalculator: FilterThresholdCalculator = new FilterThresholdCalculator(); // collect rasters of the active parameter to calculate thresholds let rasterList: Raster[] = []; if (filter.useTimeseries) { for (let date of this.getAllowedDates()) { rasterList.push(this.getCoverage(date, this.activeParameter).data); } } else { rasterList.push(this.getCoverage().data); } filterCalculator.determineStatisticalThresholds(rasterList, activeParam.getFill()); if (!filter.isProtected) { filter.min = filterCalculator.getMin(); filter.max = filterCalculator.getMax(); } filter.median = filterCalculator.getMedian(); filter.quartileQ1 = filterCalculator.getQuartileQ1(); filter.quartileQ3 = filterCalculator.getQuartileQ3(); filter.attribute = this.activeParameter; filter.setThreshold(filter.min, filter.max); if (!this.filter) { this.setFilter(filter); } } public setParameter(parameter: string): void { this.activeParameter = parameter; this.bootstrapFilter() this.update(); } public setParameterAndGradient(parameter: string, gradient: Gradient): void { this.activeParameter = parameter; this.bootstrapFilter(); this.setGradient(gradient); } public getFilter(): Filter | undefined { return this.filter; } private filterObserverRegistration: ICancel[] = []; public setFilter(filter: Filter): void { this.filter = filter; while (this.filterObserverRegistration.length > 0) { this.filterObserverRegistration.pop().cancel(); } if (filter) { // the filter thresholds changed - update the rendered coverage this.filterObserverRegistration.push(filter.on('changed:threshold', () => { this.update(); } )); // the filter use timeseries property changed - update thresholds this.filterObserverRegistration.push(filter.on('changed:useTimeseries', () => { this.bootstrapFilter(); } )); } this.update(); } public getImageInternal(extent): RenderedConverage | null { if (intersects(extent, (this._image as ImageBase).getExtent())) { return this._image; } return null; } public getHistogram(numBuckets: number, sampleBounds: [number, number] = [NaN, NaN]): Histogram { let coverage = this.getCoverage(); if (coverage) { return coverage.getHistogram(numBuckets, this.filter, sampleBounds); } return undefined; } public getHistograms(numBuckets: number, sampleBounds: [number, number] = [NaN, NaN]): Histogram[] { let hists: Histogram[] = []; for (let date of this.getAllowedDates()) { let coverage = this.getCoverage(date); if (coverage) { hists.push(coverage.getHistogram(numBuckets, this.filter, sampleBounds)); } } return hists; } public update(): void { let coverage = this.getCoverage(); if (coverage) { this._image.renderCoverage(coverage, this.gradient, this.filter); this.changed(); } } public getCoverage(date?: Date, parameter?: string): Coverage { let coverageMap = this.dateCoverageMap.get(date ? date : this.activeDate); if (coverageMap) { return coverageMap.get(parameter ? parameter : this.activeParameter); } return undefined; } public toJson(activeDateOnly: boolean = false, activeParameterOnly: boolean = false, filtered: boolean = false): object { let netcdfObject: object = {}; // always return epsg:4326 projection netcdfObject["crs"] = 'EPSG:4326'; netcdfObject["extent"] = transformExtent(this.extent, this.projection, 'EPSG:4326'); let params: NumericalParameter[] = activeParameterOnly ? [this.getActiveParameter()] : this.getAvailableParameters(); let dates: Date[] = activeDateOnly ? [this.activeDate] : this.getAllowedDates(); let filter: Filter = filtered ? this.getFilter() : undefined; let parameters: object[] = []; for (let p of params) { let parameter: object = { name: p.getName(), fill: p.hasFill() ? p.getFill() : Number.NaN, min: p.getMin(), max: p.getMax() } let coverages: object[] = []; for (let d of dates) { let coverage = this.getCoverage(d, p.getName()); if (coverage) { coverages.push(coverage.toJson(filter)); } } parameter['coverages'] = coverages; parameters.push(parameter); } netcdfObject["parameters"] = parameters; return netcdfObject; } public extractValues ( extent: Extent ): DateMap<number[]> { const values: DateMap<number[]> = new DateMap() // check whether the given extent is relevant at all if ( !intersects(extent, this.extent) ) { // the requested extent is outside of this raster source - return empty map return values } // determine pixel coordinates to extract // identify coverage dimensions const width = this.getCoverage().width const height = this.getCoverage().height const invertYAxis = this.getCoverage().invertYAxis // convert to coverage projection const coverageExtractExtent = transformExtent(extent, 'EPSG:3857', this.projection) // limit the extraction extent to the coverage extent coverageExtractExtent[0] = Math.max(coverageExtractExtent[0], this.coverageExtent[0]) coverageExtractExtent[1] = Math.max(coverageExtractExtent[1], this.coverageExtent[1]) coverageExtractExtent[2] = Math.min(coverageExtractExtent[2], this.coverageExtent[2]) coverageExtractExtent[3] = Math.min(coverageExtractExtent[3], this.coverageExtent[3]) // transform to pixel coordinates via resolution const xRes = (this.coverageExtent[2] - this.coverageExtent[0]) / width const yRes = (this.coverageExtent[3] - this.coverageExtent[1]) / height const minX = Math.floor((coverageExtractExtent[0] - this.coverageExtent[0]) / xRes) const maxX = Math.ceil((coverageExtractExtent[2] - this.coverageExtent[0]) / xRes) let minY = Math.floor(height - ((coverageExtractExtent[3] - this.coverageExtent[1]) / yRes)) let maxY = Math.ceil(height - ((coverageExtractExtent[1] - this.coverageExtent[1]) / yRes)) if ( invertYAxis ) { minY = Math.floor((coverageExtractExtent[1] - this.coverageExtent[1]) / yRes) maxY = Math.floor((coverageExtractExtent[3] - this.coverageExtent[1]) / yRes) } const pixelExtent: Extent = [minX, minY, maxX, maxY] // loop over all coverages and extract pixel values for pixel extent this.getAllowedDates().forEach((date) => { const coverage: Coverage = this.getCoverage(date) const coverageValues: number[] = coverage.extractExtent(pixelExtent, this.filter) values.set(date, coverageValues) }) return values } }