251 lines
8.0 KiB
JavaScript
251 lines
8.0 KiB
JavaScript
/* Copyright (c) 2021 Jean-Marc VIGLINO,
|
|
released under the CeCILL-B license (French BSD license)
|
|
*/
|
|
import ol_source_ImageCanvas from 'ol/source/ImageCanvas.js'
|
|
import ol_ext_Worker from '../util/Worker.js'
|
|
|
|
/** Inverse distance weighting interpolated source - Shepard's method
|
|
* @see https://en.wikipedia.org/wiki/Inverse_distance_weighting
|
|
* @constructor
|
|
* @extends {ol_source_ImageCanvas}
|
|
* @fire drawstart
|
|
* @fire drawend
|
|
* @param {*} [options]
|
|
* @param {ol.source.vector} options.source a source to interpolate
|
|
* @param {function} [options.getColor] a function that takes a value and returns a color (as an Array [r,g,b,a])
|
|
* @param {boolean} [options.useWorker=false] use worker to calculate the distance map (may cause flickering on small data sets). Source will fire drawstart, drawend while calculating
|
|
* @param {Object} [options.lib] Functions that will be made available to operations run in a worker
|
|
* @param {number} [options.scale=4] scale factor, use large factor to enhance performances (but minor accuracy)
|
|
* @param {number} [options.maxD] maximum distance in proj units to compute (default +Infinity).
|
|
* @param {string|function} options.weight The feature attribute to use for the weight or a function that returns a weight from a feature. Weight values should range from 0 to 100. Default use the weight attribute of the feature.
|
|
*/
|
|
var ol_source_IDW = class olsourceIDW extends ol_source_ImageCanvas {
|
|
constructor(options) {
|
|
options = options || {};
|
|
|
|
// Draw image on canvas
|
|
options.canvasFunction = function (extent, resolution, pixelRatio, size) {
|
|
return this.calculateImage(extent, resolution, pixelRatio, size);
|
|
};
|
|
super(options);
|
|
|
|
this._source = options.source;
|
|
this._canvas = document.createElement('canvas');
|
|
this._source.on(['addfeature', 'removefeature', 'clear', 'removefeature'], function () {
|
|
this.changed();
|
|
}.bind(this));
|
|
|
|
if (typeof(options.getColor) === 'function') this.getColor = options.getColor
|
|
|
|
if (options.useWorker) {
|
|
var lib = {
|
|
hue2rgb: this.hue2rgb,
|
|
getColor: this.getColor
|
|
}
|
|
for (let f in options.useWorker) {
|
|
lib[f] = options.useWorker[f];
|
|
}
|
|
this.worker = new ol_ext_Worker(this.computeImage, {
|
|
onMessage: this.onImageData.bind(this),
|
|
lib: lib
|
|
});
|
|
}
|
|
this._position = { extent: [], resolution: 0 };
|
|
this.set('scale', parseFloat(options.scale) || 4);
|
|
this.set('maxD', parseFloat(options.maxD) || 0)
|
|
this._weight = typeof (options.weight) === 'function' ? options.weight : function (f) { return f.get(options.weight || 'weight'); };
|
|
}
|
|
/** Set IDW scale
|
|
* @param {number} scale
|
|
*/
|
|
setScale(scale) {
|
|
this.set('scale', parseFloat(scale) || 4);
|
|
this.changed();
|
|
}
|
|
/** Set IDW max distance
|
|
* @param {number} [dist] max distance in proj units
|
|
*/
|
|
setMaxD(dist) {
|
|
this.set('maxD', parseFloat(dist) || 0);
|
|
this.changed();
|
|
}
|
|
/** Get the source
|
|
*/
|
|
getSource() {
|
|
return this._source;
|
|
}
|
|
/** Get image value at coord (RGBA)
|
|
* @param {l.coordinate} coord
|
|
* @return {Uint8ClampedArray}
|
|
*/
|
|
getValue(coord) {
|
|
if (!this._canvas) return null;
|
|
var pt = this.transform(coord);
|
|
var v = this._canvas.getContext('2d').getImageData(Math.round(pt[0]), Math.round(pt[1]), 1, 1).data;
|
|
return (v);
|
|
}
|
|
/** Convert hue to rgb factor
|
|
* @param {number} h
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
hue2rgb(h) {
|
|
h = (h + 6) % 6;
|
|
if (h < 1) return Math.round(h * 255);
|
|
if (h < 3) return 255;
|
|
if (h < 4) return Math.round((4 - h) * 255);
|
|
return 0;
|
|
}
|
|
/** Get color for a value. Return an array of RGBA values.
|
|
* @param {number} v value
|
|
* @returns {Array<number>}
|
|
* @api
|
|
*/
|
|
getColor(v) {
|
|
// Get hue
|
|
var h = 4 - (0.04 * v);
|
|
// Convert to RGB
|
|
return [
|
|
this.hue2rgb(h + 2),
|
|
this.hue2rgb(h),
|
|
this.hue2rgb(h - 2),
|
|
255
|
|
];
|
|
}
|
|
/** Compute image data
|
|
* @param {Object} e
|
|
*/
|
|
computeImage(e) {
|
|
var pts = e.data.pts;
|
|
var width = e.data.width;
|
|
var height = e.data.height;
|
|
var imageData = new Uint8ClampedArray(width * height * 4);
|
|
var dm = e.data.maxD * e.data.maxD;
|
|
// Compute image
|
|
var x, y;
|
|
for (y = 0; y < height; y++) {
|
|
for (x = 0; x < width; x++) {
|
|
var t = 0, b = 0;
|
|
for (var i = 0; i < pts.length; ++i) {
|
|
var dx = x - pts[i][0];
|
|
var dy = y - pts[i][1];
|
|
var d = dx * dx + dy * dy;
|
|
|
|
if (dm && d > dm) {
|
|
continue;
|
|
}
|
|
|
|
// Inverse distance weighting - Shepard's method
|
|
if (d === 0) {
|
|
b = 1;
|
|
t = pts[i][2];
|
|
break;
|
|
}
|
|
var inv = 1 / (d * d);
|
|
t += inv * pts[i][2];
|
|
b += inv;
|
|
}
|
|
if (t>0) {
|
|
// Set color
|
|
var color = this.getColor(t / b);
|
|
// Convert to RGB
|
|
var pos = (y * width + x) * 4;
|
|
imageData[pos] = color[0];
|
|
imageData[pos + 1] = color[1];
|
|
imageData[pos + 2] = color[2];
|
|
imageData[pos + 3] = color[3];
|
|
}
|
|
}
|
|
}
|
|
return { type: 'image', data: imageData, width: width, height: height };
|
|
}
|
|
/** Calculate IDW at extent / resolution
|
|
* @param {ol/extent/Extent} extent
|
|
* @param {number} resolution
|
|
* @param {number} pixelRatio
|
|
* @param {ol/size/Size} size
|
|
* @return {HTMLCanvasElement}
|
|
* @private
|
|
*/
|
|
calculateImage(extent, resolution, pixelRatio, size) {
|
|
if (!this._source) return this._canvas;
|
|
if (this._updated) {
|
|
this._updated = false;
|
|
return this._canvas;
|
|
}
|
|
|
|
// Calculation canvas at small resolution
|
|
var width = Math.round(size[0] / (this.get('scale') * pixelRatio));
|
|
var height = Math.round(size[1] / (this.get('scale') * pixelRatio));
|
|
|
|
// Transform coords to pixel / value
|
|
var pts = [];
|
|
var dw = width / (extent[2] - extent[0]);
|
|
var dh = height / (extent[1] - extent[3]);
|
|
var tr = this.transform = function (xy, v) {
|
|
return [
|
|
(xy[0] - extent[0]) * dw,
|
|
(xy[1] - extent[3]) * dh,
|
|
v
|
|
];
|
|
};
|
|
// Get features / weight
|
|
this._source.getFeatures().forEach(function (f) {
|
|
pts.push(tr(f.getGeometry().getFirstCoordinate(), this._weight(f)));
|
|
}.bind(this));
|
|
|
|
var message = {
|
|
pts: pts,
|
|
width: width,
|
|
height: height,
|
|
maxD: this.get('maxD') ? this.get('maxD') / this.get('scale') / resolution : 0,
|
|
resolution: resolution
|
|
};
|
|
if (this.worker) {
|
|
// kill old worker and star new one
|
|
this.worker.postMessage(message, true);
|
|
this.dispatchEvent({ type: 'drawstart' });
|
|
// Move the canvas position meanwhile
|
|
if (this._canvas.width !== Math.round(size[0])
|
|
|| this._canvas.height !== Math.round(size[1])
|
|
|| this._position.resolution !== resolution
|
|
|| this._position.extent[0] !== extent[0]
|
|
|| this._position.extent[1] !== extent[1]) {
|
|
this._canvas.width = Math.round(size[0]);
|
|
this._canvas.height = Math.round(size[1]);
|
|
}
|
|
this._position.extent = extent;
|
|
this._position.resolution = resolution;
|
|
} else {
|
|
this._canvas.width = Math.round(size[0]);
|
|
this._canvas.height = Math.round(size[1]);
|
|
var imageData = this.computeImage({ data: message });
|
|
this.onImageData(imageData);
|
|
}
|
|
|
|
return this._canvas;
|
|
}
|
|
/** Display data when ready
|
|
* @private
|
|
*/
|
|
onImageData(imageData) {
|
|
// Calculation canvas at small resolution
|
|
var canvas = this._internal = document.createElement('canvas');
|
|
canvas.width = imageData.width;
|
|
canvas.height = imageData.height;
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.putImageData(new ImageData(imageData.data, imageData.width, imageData.height), 0, 0);
|
|
|
|
// Draw full resolution canvas
|
|
this._canvas.getContext('2d').drawImage(canvas, 0, 0, this._canvas.width, this._canvas.height);
|
|
// Force redraw
|
|
if (this.worker) {
|
|
this.dispatchEvent({ type: 'drawend' });
|
|
this._updated = true;
|
|
this.changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default ol_source_IDW
|