ol-ext
Version:
A set of cool extensions for OpenLayers (ol) in node modules structure
251 lines (237 loc) • 8.44 kB
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