UNPKG

leaflet.tilelayer.glcolorscale

Version:

Custom Leaflet TileLayer using WebGL to colorize floating-point pixels according to a specified color scale

216 lines (199 loc) 6.83 kB
import { memoize } from 'lodash-es'; import REGL from 'regl'; import { decode } from 'upng-js'; import { Color, Dictionary, TextureBounds, TileCoordinates, WebGLColorStop, } from './types'; /** * Converts a color scale (or array of SentinelValues) to a format usable by WebGL. */ export function convertColorScale(colorScale: Color[]): WebGLColorStop[] { return colorScale.map(({ color, offset }) => ({ color: colorStringToWebGLFloats(color), offset, })); } const RGB_REGEX = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/; /** * Parses a color string of the form 'rgb({rVal}, {gVal}, {bVal})' and converts the resulting values * to a Vec4 consumable by WebGL. Each color value is normalized to the range 0.0 to 1.0. */ export function colorStringToWebGLFloats(rgb: string): REGL.Vec4 { if (rgb === 'transparent') { return [1, 1, 1, 0]; } const match = rgb.match(RGB_REGEX); if (match === null) { throw new Error(`'${rgb}' is not a valid RGB color expression.`); } const [, r, g, b] = match; return [+r / 255, +g / 255, +b / 255, 1]; } /** * Create an object representing the elements and properties of a WebGL struct array to be passed * to Regl. Each property is a "dynamic prop" in Regl parlance. That is, it's a function that is * evaluated when the drawing function is called. When each dynamic prop (function) is evaluated, * it's passed as second argument a `props` object (similar to `props` in a React component). */ export function bindStructArray< Struct extends Dictionary<any>, Props extends Dictionary<any[]> = {} >( structPropertyNames: Array<keyof Struct>, defaultValue: Struct, maxArrayLength: number, glslIdentifier: string, propName: keyof Props = (glslIdentifier as keyof Props), ) { const output = {} as Dictionary<any>; for (let i = 0; i < maxArrayLength; ++i) { for (const key of structPropertyNames) { output[`${glslIdentifier}[${i}].${key}`] = (_: any, props: Props) => { const inputArray = props[propName]; return ( i < inputArray.length ? inputArray[i][key] : defaultValue[key] ); }; } } return output; } export function machineIsLittleEndian() { const uint8Array = new Uint8Array([0xAA, 0xBB]); const uint16array = new Uint16Array(uint8Array.buffer); return uint16array[0] === 0xBBAA; } /** * Cribbed from Python's built-in `range` function. */ export function range(...args: number[]) { if (args.length === 1) { const [until] = args; return new Array(until).fill(undefined).map((_, i) => i); } else { const [from, until, step = 1] = args; if (step === 0) { throw new Error('Argument step must be nonzero.'); } const output = []; for (let val = from; (step > 0) ? val < until : val > until; val += step) { output.push(val); } return output; } } export async function fetchPNGData(url: string, nodataValue: number, tileDimension: number): Promise<Uint8Array> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "arraybuffer"; xhr.addEventListener('load', () => { resolve(xhr.response); }); xhr.addEventListener('error', reject); xhr.send(null); }).then((data: ArrayBuffer) => { return new Uint8Array(decode(data).data); }).catch(() => createNoDataTile(nodataValue, tileDimension)); } /** * The matrix output by this function transforms coordinates in pixel space within the drawing * buffer (with upper left corner (0, 0) and lower right corner (buffer width, buffer height)) * to WebGL "clipspace", with upper left corner (-1, 1) and lower right corner (1, -1). */ export function getTransformMatrix( drawingBufferWidth: number, drawingBufferHeight: number, ): REGL.Mat4 { // To scale horizontally, divide by width (in pixels) and multiply by 2, because width is 2 in clipspace. const sx = 2 / drawingBufferWidth; // To scale vertically, divide by height (in pixels) and multiply by -2, because height is 2 in clipspace, // and the direction is flipped (positive is up, negative is down). const sy = -2 / drawingBufferHeight; // We translate by -1 horizontally (so the range 0 to 2 maps to the range -1 to 1). const tx = -1; // We translate by 1 horizontally (so the range -2 to 0 maps to the range -1 to 1). const ty = 1; // Matrix must be in column-major order for WebGL. return [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1, ]; } /** * From a TextureBounds object, this function generates the four vertices WebGL needs to draw the * corresponding rectangle (as two conjoined triangles generated with the triangle strip primitive). */ export function getTexCoordVertices(textureBounds: TextureBounds): REGL.Vec2[] { const [{ x: left, y: top }, { x: right, y: bottom }] = textureBounds; return [ [left, top ], [right, top ], [left, bottom], [right, bottom], ]; } /** * Produces a Promise that resolves when the desired `duration` has expired. */ export function Timer(duration: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, duration)); } /** * Useful for sorting TileCoorindates objects. */ export function compareTileCoordinates(a: TileCoordinates, b: TileCoordinates): number { const z = a.z - b.z; const x = a.x - b.x; const y = a.y - b.y; if (z !== 0) { // First compare z values. return z; } else if (x !== 0) { // If z values are the same, compare x values. return x; } else { // If x values are the same, compare y values. return y; } } /** * Determines whether two arrays of TileCoordinates are the same. */ export function sameTiles(a: TileCoordinates[], b: TileCoordinates[]): boolean { return ( // arrays are of the same length a.length === b.length // and corresponding elements have the same tile coordinates && a.every((tileA, index) => compareTileCoordinates(tileA, b[index]) === 0) ); } const createNoDataTile = memoize((nodataValue: number, tileDimension: number = 256): Uint8Array => { // Create a float 32 array. const float32Tile = new Float32Array(tileDimension * tileDimension); // Fill the tile array with the no data value float32Tile.fill(nodataValue); // return the no data tile. return new Uint8Array(float32Tile.buffer); }); /** * Force TypeScript to interpret value `val` as type `T`. */ export function staticCast<T>(val: any): T { return val as T; } /** * Add one or more macro definitions to a GLSL source string. */ export function defineMacros(src: string, macros: { [key: string]: any }): string { const defs = Object.keys(macros).map((key) => `#define ${key} ${macros[key]}\n`).join(''); return `${defs}\n${src}`; }