dasf-web
Version:
Web frontend components for the data analytics software framework (DASF)
1,470 lines (1,232 loc) • 48.5 kB
text/typescript
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
}
}