UNPKG

leaflet.tilelayer.glcolorscale

Version:

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

648 lines (622 loc) 70.5 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var L = require('leaflet'); var lodashEs = require('lodash-es'); var REGL = _interopDefault(require('regl')); var upngJs = require('upng-js'); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css = ".gl-tilelayer-tile {\n -ms-interpolation-mode: nearest-neighbor;\n image-rendering: -moz-crisp-edges;\n image-rendering: pixelated;\n image-rendering: crisp-edges;\n}\n"; styleInject(css); var SCALE_MAX_LENGTH = 16; var SENTINEL_MAX_LENGTH = 16; var CLEAR_COLOR = [0, 0, 0, 0]; var DEFAULT_COLOR_STOP = { color: CLEAR_COLOR, offset: 0, }; var vertDouble = "#define GLSLIFY 1\nuniform mat4 transformMatrix;\n\nattribute vec2 position;\nattribute vec2 texCoordA;\nattribute vec2 texCoordB;\n\nvarying vec2 vTexCoordA;\nvarying vec2 vTexCoordB;\n\nvoid main() {\n vTexCoordA = texCoordA;\n vTexCoordB = texCoordB;\n gl_Position = transformMatrix * vec4(position, 0.0, 1.0);\n}\n"; // eslint-disable-line var vertSingle = "#define GLSLIFY 1\nuniform mat4 transformMatrix;\n\nattribute vec2 position;\nattribute vec2 texCoord;\n\nvarying vec2 vTexCoord;\n\nvoid main() {\n vTexCoord = texCoord;\n gl_Position = transformMatrix * vec4(position, 0.0, 1.0);\n}\n"; // eslint-disable-line var fragInterpolateColor = "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#define GLSLIFY 1\n#endif\n\n#define TRANSPARENT vec4(0.0)\n\n// Denormalize 8-bit color channels to integers in the range 0 to 255.\nivec4 floatsToBytes(vec4 inputFloats, bool littleEndian) {\n ivec4 bytes = ivec4(inputFloats * 255.0);\n return (\n littleEndian\n ? bytes.abgr\n : bytes\n );\n}\n\n// Break the four bytes down into an array of 32 bits.\nvoid bytesToBits(const in ivec4 bytes, out bool bits[32]) {\n for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {\n float acc = float(bytes[channelIndex]);\n for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {\n float powerOfTwo = exp2(float(indexInByte));\n bool bit = acc >= powerOfTwo;\n bits[channelIndex * 8 + (7 - indexInByte)] = bit;\n acc = mod(acc, powerOfTwo);\n }\n }\n}\n\n// Compute the exponent of the 32-bit float.\nfloat getExponent(bool bits[32]) {\n const int startIndex = 1;\n const int bitStringLength = 8;\n const int endBeforeIndex = startIndex + bitStringLength;\n float acc = 0.0;\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Compute the mantissa of the 32-bit float.\nfloat getMantissa(bool bits[32], bool subnormal) {\n const int startIndex = 9;\n const int bitStringLength = 23;\n const int endBeforeIndex = startIndex + bitStringLength;\n // Leading/implicit/hidden bit convention:\n // If the number is not subnormal (with exponent 0), we add a leading 1 digit.\n float acc = float(!subnormal) * exp2(float(bitStringLength));\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Parse the float from its 32 bits.\nfloat bitsToFloat(bool bits[32]) {\n float signBit = float(bits[0]) * -2.0 + 1.0;\n float exponent = getExponent(bits);\n bool subnormal = abs(exponent - 0.0) < 0.01;\n float mantissa = getMantissa(bits, subnormal);\n float exponentBias = 127.0;\n return signBit * mantissa * exp2(exponent - exponentBias - 23.0);\n}\n\n// Decode a 32-bit float from the RGBA color channels of a texel.\nfloat rgbaToFloat(vec4 texelRGBA, bool littleEndian) {\n ivec4 rgbaBytes = floatsToBytes(texelRGBA, littleEndian);\n bool bits[32];\n bytesToBits(rgbaBytes, bits);\n return bitsToFloat(bits);\n}\n\n#ifndef RELATIVE_TOLERANCE\n#define RELATIVE_TOLERANCE 0.0001\n#endif\n\nbool isCloseEnough(float a, float b) {\n return abs(a - b) <= max(abs(a), abs(b)) * RELATIVE_TOLERANCE;\n}\n\nstruct ScaleStop {\n float offset;\n vec4 color;\n};\n\n#ifndef DEFAULT_COLOR\n#define DEFAULT_COLOR vec4(0.0)\n#endif\n\n#ifndef SCALE_MAX_LENGTH\n#define SCALE_MAX_LENGTH 16\n#endif\n\n#ifndef SENTINEL_MAX_LENGTH\n#define SENTINEL_MAX_LENGTH 16\n#endif\n\nvec4 computeColor(\n float inputVal,\n ScaleStop colorScale[SCALE_MAX_LENGTH],\n ScaleStop sentinelValues[SENTINEL_MAX_LENGTH],\n int colorScaleLength,\n int sentinelValuesLength\n) {\n // Compare the value against any sentinel values, if defined.\n if (sentinelValuesLength > 0) {\n for (int i_0 = 0; i_0 < SENTINEL_MAX_LENGTH; ++i_0) {\n if (i_0 == sentinelValuesLength) {\n break;\n }\n ScaleStop sentinel = sentinelValues[i_0];\n if (isCloseEnough(inputVal, sentinel.offset)) {\n return sentinel.color;\n }\n }\n }\n\n // Do linear interpolation using the color scale, if defined.\n if (colorScaleLength > 0) {\n // If value below color scale range, clamp to lowest color stop.\n if (inputVal < colorScale[0].offset) {\n return colorScale[0].color;\n } else {\n for (int i = 0; i < SCALE_MAX_LENGTH; ++i) {\n // If value above color scale range, clamp to highest color stop.\n if (i == colorScaleLength) {\n return colorScale[i - 1].color;\n } else if (inputVal <= colorScale[i + 1].offset) {\n float percent = (inputVal - colorScale[i].offset)\n / (colorScale[i + 1].offset - colorScale[i].offset);\n return mix(colorScale[i].color, colorScale[i + 1].color, percent);\n }\n }\n }\n }\n\n return DEFAULT_COLOR;\n}\n\nuniform sampler2D textureA;\nuniform ScaleStop colorScaleA[SCALE_MAX_LENGTH];\nuniform int colorScaleLengthA;\nuniform ScaleStop sentinelValuesA[SENTINEL_MAX_LENGTH];\nuniform int sentinelValuesLengthA;\n\nuniform sampler2D textureB;\nuniform ScaleStop colorScaleB[SCALE_MAX_LENGTH];\nuniform int colorScaleLengthB;\nuniform ScaleStop sentinelValuesB[SENTINEL_MAX_LENGTH];\nuniform int sentinelValuesLengthB;\n\nuniform float nodataValue;\nuniform bool littleEndian;\nuniform float interpolationFraction;\n\nvarying vec2 vTexCoordA;\nvarying vec2 vTexCoordB;\n\nvoid main() {\n if (interpolationFraction <= 0.0) {\n vec4 rgbaFloats = texture2D(textureA, vTexCoordA);\n float pixelFloatValue = rgbaToFloat(rgbaFloats, littleEndian);\n if (isCloseEnough(pixelFloatValue, nodataValue)) {\n discard;\n }\n gl_FragColor = computeColor(pixelFloatValue, colorScaleA, sentinelValuesA, colorScaleLengthA, sentinelValuesLengthA);\n } else if (interpolationFraction >= 1.0) {\n vec4 rgbaFloats = texture2D(textureB, vTexCoordB);\n float pixelFloatValue = rgbaToFloat(rgbaFloats, littleEndian);\n if (isCloseEnough(pixelFloatValue, nodataValue)) {\n discard;\n }\n gl_FragColor = computeColor(pixelFloatValue, colorScaleB, sentinelValuesB, colorScaleLengthB, sentinelValuesLengthB);\n } else {\n vec4 rgbaFloatsA = texture2D(textureA, vTexCoordA);\n float pixelFloatValueA = rgbaToFloat(rgbaFloatsA, littleEndian);\n vec4 rgbaFloatsB = texture2D(textureB, vTexCoordB);\n float pixelFloatValueB = rgbaToFloat(rgbaFloatsB, littleEndian);\n vec4 colorA = (\n isCloseEnough(pixelFloatValueA, nodataValue)\n ? TRANSPARENT\n : computeColor(pixelFloatValueA, colorScaleA, sentinelValuesA, colorScaleLengthA, sentinelValuesLengthA)\n );\n vec4 colorB = (\n isCloseEnough(pixelFloatValueB, nodataValue)\n ? TRANSPARENT\n : computeColor(pixelFloatValueB, colorScaleB, sentinelValuesB, colorScaleLengthB, sentinelValuesLengthB)\n );\n gl_FragColor = mix(colorA, colorB, interpolationFraction);\n }\n}\n"; // eslint-disable-line var fragInterpolateValue = "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#define GLSLIFY 1\n#endif\n\n#define TRANSPARENT vec4(0.0)\n\n// Denormalize 8-bit color channels to integers in the range 0 to 255.\nivec4 floatsToBytes(vec4 inputFloats, bool littleEndian) {\n ivec4 bytes = ivec4(inputFloats * 255.0);\n return (\n littleEndian\n ? bytes.abgr\n : bytes\n );\n}\n\n// Break the four bytes down into an array of 32 bits.\nvoid bytesToBits(const in ivec4 bytes, out bool bits[32]) {\n for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {\n float acc = float(bytes[channelIndex]);\n for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {\n float powerOfTwo = exp2(float(indexInByte));\n bool bit = acc >= powerOfTwo;\n bits[channelIndex * 8 + (7 - indexInByte)] = bit;\n acc = mod(acc, powerOfTwo);\n }\n }\n}\n\n// Compute the exponent of the 32-bit float.\nfloat getExponent(bool bits[32]) {\n const int startIndex = 1;\n const int bitStringLength = 8;\n const int endBeforeIndex = startIndex + bitStringLength;\n float acc = 0.0;\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Compute the mantissa of the 32-bit float.\nfloat getMantissa(bool bits[32], bool subnormal) {\n const int startIndex = 9;\n const int bitStringLength = 23;\n const int endBeforeIndex = startIndex + bitStringLength;\n // Leading/implicit/hidden bit convention:\n // If the number is not subnormal (with exponent 0), we add a leading 1 digit.\n float acc = float(!subnormal) * exp2(float(bitStringLength));\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Parse the float from its 32 bits.\nfloat bitsToFloat(bool bits[32]) {\n float signBit = float(bits[0]) * -2.0 + 1.0;\n float exponent = getExponent(bits);\n bool subnormal = abs(exponent - 0.0) < 0.01;\n float mantissa = getMantissa(bits, subnormal);\n float exponentBias = 127.0;\n return signBit * mantissa * exp2(exponent - exponentBias - 23.0);\n}\n\n// Decode a 32-bit float from the RGBA color channels of a texel.\nfloat rgbaToFloat(vec4 texelRGBA, bool littleEndian) {\n ivec4 rgbaBytes = floatsToBytes(texelRGBA, littleEndian);\n bool bits[32];\n bytesToBits(rgbaBytes, bits);\n return bitsToFloat(bits);\n}\n\n#ifndef RELATIVE_TOLERANCE\n#define RELATIVE_TOLERANCE 0.0001\n#endif\n\nbool isCloseEnough(float a, float b) {\n return abs(a - b) <= max(abs(a), abs(b)) * RELATIVE_TOLERANCE;\n}\n\nstruct ScaleStop {\n float offset;\n vec4 color;\n};\n\n#ifndef DEFAULT_COLOR\n#define DEFAULT_COLOR vec4(0.0)\n#endif\n\n#ifndef SCALE_MAX_LENGTH\n#define SCALE_MAX_LENGTH 16\n#endif\n\n#ifndef SENTINEL_MAX_LENGTH\n#define SENTINEL_MAX_LENGTH 16\n#endif\n\nvec4 computeColor(\n float inputVal,\n ScaleStop colorScale[SCALE_MAX_LENGTH],\n ScaleStop sentinelValues[SENTINEL_MAX_LENGTH],\n int colorScaleLength_0,\n int sentinelValuesLength_0\n) {\n // Compare the value against any sentinel values, if defined.\n if (sentinelValuesLength_0 > 0) {\n for (int i_0 = 0; i_0 < SENTINEL_MAX_LENGTH; ++i_0) {\n if (i_0 == sentinelValuesLength_0) {\n break;\n }\n ScaleStop sentinel = sentinelValues[i_0];\n if (isCloseEnough(inputVal, sentinel.offset)) {\n return sentinel.color;\n }\n }\n }\n\n // Do linear interpolation using the color scale, if defined.\n if (colorScaleLength_0 > 0) {\n // If value below color scale range, clamp to lowest color stop.\n if (inputVal < colorScale[0].offset) {\n return colorScale[0].color;\n } else {\n for (int i = 0; i < SCALE_MAX_LENGTH; ++i) {\n // If value above color scale range, clamp to highest color stop.\n if (i == colorScaleLength_0) {\n return colorScale[i - 1].color;\n } else if (inputVal <= colorScale[i + 1].offset) {\n float percent = (inputVal - colorScale[i].offset)\n / (colorScale[i + 1].offset - colorScale[i].offset);\n return mix(colorScale[i].color, colorScale[i + 1].color, percent);\n }\n }\n }\n }\n\n return DEFAULT_COLOR;\n}\n\nuniform ScaleStop colorScale[SCALE_MAX_LENGTH];\nuniform int colorScaleLength;\n\nuniform ScaleStop sentinelValues[SENTINEL_MAX_LENGTH];\nuniform int sentinelValuesLength;\n\nuniform float nodataValue;\nuniform sampler2D textureA;\nuniform sampler2D textureB;\nuniform bool littleEndian;\nuniform float interpolationFraction;\n\nvarying vec2 vTexCoordA;\nvarying vec2 vTexCoordB;\n\nbool isSentinelValue(ScaleStop sentinelValues[SENTINEL_MAX_LENGTH], int len, float value) {\n for (int i = 0; i < SENTINEL_MAX_LENGTH; ++i) {\n if (i == len) {\n break;\n }\n if (isCloseEnough(sentinelValues[i].offset, value)) {\n return true;\n }\n }\n return false;\n}\n\nvoid main() {\n if (interpolationFraction <= 0.0) {\n vec4 rgbaFloats = texture2D(textureA, vTexCoordA);\n float pixelFloatValue = rgbaToFloat(rgbaFloats, littleEndian);\n if (isCloseEnough(pixelFloatValue, nodataValue)) {\n discard;\n }\n gl_FragColor = computeColor(pixelFloatValue, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength);\n } else if (interpolationFraction >= 1.0) {\n vec4 rgbaFloats = texture2D(textureB, vTexCoordB);\n float pixelFloatValue = rgbaToFloat(rgbaFloats, littleEndian);\n if (isCloseEnough(pixelFloatValue, nodataValue)) {\n discard;\n }\n gl_FragColor = computeColor(pixelFloatValue, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength);\n } else {\n vec4 rgbaFloatsA = texture2D(textureA, vTexCoordA);\n float pixelFloatValueA = rgbaToFloat(rgbaFloatsA, littleEndian);\n vec4 rgbaFloatsB = texture2D(textureB, vTexCoordB);\n float pixelFloatValueB = rgbaToFloat(rgbaFloatsB, littleEndian);\n bool aIsNodata = isCloseEnough(pixelFloatValueA, nodataValue);\n bool bIsNodata = isCloseEnough(pixelFloatValueB, nodataValue);\n if (aIsNodata && bIsNodata) {\n discard;\n } else if (\n aIsNodata\n || bIsNodata\n || colorScaleLength == 0\n || isSentinelValue(sentinelValues, sentinelValuesLength, pixelFloatValueA)\n || isSentinelValue(sentinelValues, sentinelValuesLength, pixelFloatValueB)\n ) {\n vec4 colorA = (\n aIsNodata\n ? TRANSPARENT\n : computeColor(pixelFloatValueA, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength)\n );\n vec4 colorB = (\n bIsNodata\n ? TRANSPARENT\n : computeColor(pixelFloatValueB, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength)\n );\n gl_FragColor = mix(colorA, colorB, interpolationFraction);\n } else {\n float interpolated = mix(pixelFloatValueA, pixelFloatValueB, interpolationFraction);\n gl_FragColor = computeColor(interpolated, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength);\n }\n }\n}\n"; // eslint-disable-line var fragSingle = "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#define GLSLIFY 1\n#endif\n\n// Denormalize 8-bit color channels to integers in the range 0 to 255.\nivec4 floatsToBytes(vec4 inputFloats, bool littleEndian) {\n ivec4 bytes = ivec4(inputFloats * 255.0);\n return (\n littleEndian\n ? bytes.abgr\n : bytes\n );\n}\n\n// Break the four bytes down into an array of 32 bits.\nvoid bytesToBits(const in ivec4 bytes, out bool bits[32]) {\n for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {\n float acc = float(bytes[channelIndex]);\n for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {\n float powerOfTwo = exp2(float(indexInByte));\n bool bit = acc >= powerOfTwo;\n bits[channelIndex * 8 + (7 - indexInByte)] = bit;\n acc = mod(acc, powerOfTwo);\n }\n }\n}\n\n// Compute the exponent of the 32-bit float.\nfloat getExponent(bool bits[32]) {\n const int startIndex = 1;\n const int bitStringLength = 8;\n const int endBeforeIndex = startIndex + bitStringLength;\n float acc = 0.0;\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Compute the mantissa of the 32-bit float.\nfloat getMantissa(bool bits[32], bool subnormal) {\n const int startIndex = 9;\n const int bitStringLength = 23;\n const int endBeforeIndex = startIndex + bitStringLength;\n // Leading/implicit/hidden bit convention:\n // If the number is not subnormal (with exponent 0), we add a leading 1 digit.\n float acc = float(!subnormal) * exp2(float(bitStringLength));\n int pow2 = bitStringLength - 1;\n for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {\n acc += float(bits[bitIndex]) * exp2(float(pow2--));\n }\n return acc;\n}\n\n// Parse the float from its 32 bits.\nfloat bitsToFloat(bool bits[32]) {\n float signBit = float(bits[0]) * -2.0 + 1.0;\n float exponent = getExponent(bits);\n bool subnormal = abs(exponent - 0.0) < 0.01;\n float mantissa = getMantissa(bits, subnormal);\n float exponentBias = 127.0;\n return signBit * mantissa * exp2(exponent - exponentBias - 23.0);\n}\n\n// Decode a 32-bit float from the RGBA color channels of a texel.\nfloat rgbaToFloat(vec4 texelRGBA, bool littleEndian) {\n ivec4 rgbaBytes = floatsToBytes(texelRGBA, littleEndian);\n bool bits[32];\n bytesToBits(rgbaBytes, bits);\n return bitsToFloat(bits);\n}\n\n#ifndef RELATIVE_TOLERANCE\n#define RELATIVE_TOLERANCE 0.0001\n#endif\n\nbool isCloseEnough(float a, float b) {\n return abs(a - b) <= max(abs(a), abs(b)) * RELATIVE_TOLERANCE;\n}\n\nstruct ScaleStop {\n float offset;\n vec4 color;\n};\n\n#ifndef DEFAULT_COLOR\n#define DEFAULT_COLOR vec4(0.0)\n#endif\n\n#ifndef SCALE_MAX_LENGTH\n#define SCALE_MAX_LENGTH 16\n#endif\n\n#ifndef SENTINEL_MAX_LENGTH\n#define SENTINEL_MAX_LENGTH 16\n#endif\n\nvec4 computeColor(\n float inputVal,\n ScaleStop colorScale[SCALE_MAX_LENGTH],\n ScaleStop sentinelValues[SENTINEL_MAX_LENGTH],\n int colorScaleLength_0,\n int sentinelValuesLength_0\n) {\n // Compare the value against any sentinel values, if defined.\n if (sentinelValuesLength_0 > 0) {\n for (int i_0 = 0; i_0 < SENTINEL_MAX_LENGTH; ++i_0) {\n if (i_0 == sentinelValuesLength_0) {\n break;\n }\n ScaleStop sentinel = sentinelValues[i_0];\n if (isCloseEnough(inputVal, sentinel.offset)) {\n return sentinel.color;\n }\n }\n }\n\n // Do linear interpolation using the color scale, if defined.\n if (colorScaleLength_0 > 0) {\n // If value below color scale range, clamp to lowest color stop.\n if (inputVal < colorScale[0].offset) {\n return colorScale[0].color;\n } else {\n for (int i = 0; i < SCALE_MAX_LENGTH; ++i) {\n // If value above color scale range, clamp to highest color stop.\n if (i == colorScaleLength_0) {\n return colorScale[i - 1].color;\n } else if (inputVal <= colorScale[i + 1].offset) {\n float percent = (inputVal - colorScale[i].offset)\n / (colorScale[i + 1].offset - colorScale[i].offset);\n return mix(colorScale[i].color, colorScale[i + 1].color, percent);\n }\n }\n }\n }\n\n return DEFAULT_COLOR;\n}\n\nuniform ScaleStop colorScale[SCALE_MAX_LENGTH];\nuniform int colorScaleLength;\n\nuniform ScaleStop sentinelValues[SENTINEL_MAX_LENGTH];\nuniform int sentinelValuesLength;\n\nuniform float nodataValue;\nuniform sampler2D texture;\nuniform bool littleEndian;\n\nvarying vec2 vTexCoord;\n\nvoid main() {\n vec4 rgbaFloats = texture2D(texture, vTexCoord);\n float pixelFloatValue = rgbaToFloat(rgbaFloats, littleEndian);\n if (isCloseEnough(pixelFloatValue, nodataValue)) {\n discard;\n }\n gl_FragColor = computeColor(pixelFloatValue, colorScale, sentinelValues, colorScaleLength, sentinelValuesLength);\n}\n"; // eslint-disable-line function convertColorScale(colorScale) { return colorScale.map(function (_a) { var color = _a.color, offset = _a.offset; return ({ color: colorStringToWebGLFloats(color), offset: offset, }); }); } var RGB_REGEX = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/; function colorStringToWebGLFloats(rgb) { if (rgb === 'transparent') { return [1, 1, 1, 0]; } var match = rgb.match(RGB_REGEX); if (match === null) { throw new Error("'" + rgb + "' is not a valid RGB color expression."); } var r = match[1], g = match[2], b = match[3]; return [+r / 255, +g / 255, +b / 255, 1]; } function bindStructArray(structPropertyNames, defaultValue, maxArrayLength, glslIdentifier, propName) { if (propName === void 0) { propName = glslIdentifier; } var output = {}; var _loop_1 = function (i) { var _loop_2 = function (key) { output[glslIdentifier + "[" + i + "]." + key] = function (_, props) { var inputArray = props[propName]; return (i < inputArray.length ? inputArray[i][key] : defaultValue[key]); }; }; for (var _i = 0, structPropertyNames_1 = structPropertyNames; _i < structPropertyNames_1.length; _i++) { var key = structPropertyNames_1[_i]; _loop_2(key); } }; for (var i = 0; i < maxArrayLength; ++i) { _loop_1(i); } return output; } function machineIsLittleEndian() { var uint8Array = new Uint8Array([0xAA, 0xBB]); var uint16array = new Uint16Array(uint8Array.buffer); return uint16array[0] === 0xBBAA; } function range() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (args.length === 1) { var until = args[0]; return new Array(until).fill(undefined).map(function (_, i) { return i; }); } else { var from = args[0], until = args[1], _a = args[2], step = _a === void 0 ? 1 : _a; if (step === 0) { throw new Error('Argument step must be nonzero.'); } var output = []; for (var val = from; (step > 0) ? val < until : val > until; val += step) { output.push(val); } return output; } } function fetchPNGData(url, nodataValue, tileDimension) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2, new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "arraybuffer"; xhr.addEventListener('load', function () { resolve(xhr.response); }); xhr.addEventListener('error', reject); xhr.send(null); }).then(function (data) { return new Uint8Array(upngJs.decode(data).data); }).catch(function () { return createNoDataTile(nodataValue, tileDimension); })]; }); }); } function getTransformMatrix(drawingBufferWidth, drawingBufferHeight) { var sx = 2 / drawingBufferWidth; var sy = -2 / drawingBufferHeight; var tx = -1; var ty = 1; return [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1, ]; } function getTexCoordVertices(textureBounds) { var _a = textureBounds[0], left = _a.x, top = _a.y, _b = textureBounds[1], right = _b.x, bottom = _b.y; return [ [left, top], [right, top], [left, bottom], [right, bottom], ]; } function Timer(duration) { return new Promise(function (resolve) { return setTimeout(resolve, duration); }); } function compareTileCoordinates(a, b) { var z = a.z - b.z; var x = a.x - b.x; var y = a.y - b.y; if (z !== 0) { return z; } else if (x !== 0) { return x; } else { return y; } } function sameTiles(a, b) { return (a.length === b.length && a.every(function (tileA, index) { return compareTileCoordinates(tileA, b[index]) === 0; })); } var createNoDataTile = lodashEs.memoize(function (nodataValue, tileDimension) { if (tileDimension === void 0) { tileDimension = 256; } var float32Tile = new Float32Array(tileDimension * tileDimension); float32Tile.fill(nodataValue); return new Uint8Array(float32Tile.buffer); }); function staticCast(val) { return val; } function defineMacros(src, macros) { var defs = Object.keys(macros).map(function (key) { return "#define " + key + " " + macros[key] + "\n"; }).join(''); return defs + "\n" + src; } var littleEndian = machineIsLittleEndian(); var bindStructArray$1 = bindStructArray.bind(null, ['color', 'offset'], DEFAULT_COLOR_STOP); var colorScaleUniforms = bindStructArray$1(SCALE_MAX_LENGTH, 'colorScale'); var sentinelValuesUniforms = bindStructArray$1(SENTINEL_MAX_LENGTH, 'sentinelValues'); var fragMacros = { SCALE_MAX_LENGTH: SCALE_MAX_LENGTH, SENTINEL_MAX_LENGTH: SENTINEL_MAX_LENGTH, }; function getCommonDrawConfiguration(tileSize, nodataValue) { return { uniforms: { nodataValue: nodataValue, littleEndian: littleEndian, transformMatrix: function (_a) { var viewportWidth = _a.viewportWidth, viewportHeight = _a.viewportHeight; return (getTransformMatrix(viewportWidth, viewportHeight)); }, }, attributes: { position: function (_, _a) { var canvasCoordinates = _a.canvasCoordinates; var left = canvasCoordinates[0], top = canvasCoordinates[1]; var _b = [left + tileSize, top + tileSize], right = _b[0], bottom = _b[1]; return [ [left, top], [right, top], [left, bottom], [right, bottom], ]; }, }, depth: { enable: false }, primitive: 'triangle strip', count: 4, viewport: function (_, _a) { var _b = _a.canvasSize, width = _b[0], height = _b[1]; return ({ width: width, height: height }); }, }; } function createDrawTileCommand(regl, commonConfig) { return regl(__assign({}, commonConfig, { vert: vertSingle, frag: defineMacros(fragSingle, fragMacros), uniforms: __assign({}, commonConfig.uniforms, colorScaleUniforms, sentinelValuesUniforms, { colorScaleLength: function (_, _a) { var colorScale = _a.colorScale; return colorScale.length; }, sentinelValuesLength: function (_, _a) { var sentinelValues = _a.sentinelValues; return sentinelValues.length; }, texture: function (_, _a) { var texture = _a.texture; return texture; } }), attributes: __assign({}, commonConfig.attributes, { texCoord: function (_, _a) { var textureBounds = _a.textureBounds; return getTexCoordVertices(textureBounds); } }) })); } function createDrawTileInterpolateValueCommand(regl, commonConfig) { return regl(__assign({}, commonConfig, { vert: vertDouble, frag: defineMacros(fragInterpolateValue, fragMacros), uniforms: __assign({}, commonConfig.uniforms, colorScaleUniforms, sentinelValuesUniforms, { colorScaleLength: function (_, _a) { var colorScale = _a.colorScale; return colorScale.length; }, sentinelValuesLength: function (_, _a) { var sentinelValues = _a.sentinelValues; return sentinelValues.length; }, textureA: function (_, _a) { var textureA = _a.textureA; return textureA; }, textureB: function (_, _a) { var textureB = _a.textureB; return textureB; }, interpolationFraction: function (_, _a) { var interpolationFraction = _a.interpolationFraction; return interpolationFraction; } }), attributes: __assign({}, commonConfig.attributes, { texCoordA: function (_, _a) { var textureBoundsA = _a.textureBoundsA; return getTexCoordVertices(textureBoundsA); }, texCoordB: function (_, _a) { var textureBoundsB = _a.textureBoundsB; return getTexCoordVertices(textureBoundsB); } }) })); } function createDrawTileInterpolateColorCommand(regl, commonConfig) { return regl(__assign({}, commonConfig, { vert: vertDouble, frag: defineMacros(fragInterpolateColor, fragMacros), uniforms: __assign({}, commonConfig.uniforms, bindStructArray$1(SCALE_MAX_LENGTH, 'colorScaleA'), bindStructArray$1(SCALE_MAX_LENGTH, 'colorScaleB'), bindStructArray$1(SENTINEL_MAX_LENGTH, 'sentinelValuesA'), bindStructArray$1(SENTINEL_MAX_LENGTH, 'sentinelValuesB'), { colorScaleLengthA: function (_, _a) { var colorScaleA = _a.colorScaleA; return colorScaleA.length; }, colorScaleLengthB: function (_, _a) { var colorScaleB = _a.colorScaleB; return colorScaleB.length; }, sentinelValuesLengthA: function (_, _a) { var sentinelValuesA = _a.sentinelValuesA; return sentinelValuesA.length; }, sentinelValuesLengthB: function (_, _a) { var sentinelValuesB = _a.sentinelValuesB; return sentinelValuesB.length; }, textureA: function (_, _a) { var textureA = _a.textureA; return textureA; }, textureB: function (_, _a) { var textureB = _a.textureB; return textureB; }, interpolationFraction: function (_, _a) { var interpolationFraction = _a.interpolationFraction; return interpolationFraction; } }), attributes: __assign({}, commonConfig.attributes, { texCoordA: function (_, _a) { var textureBoundsA = _a.textureBoundsA; return getTexCoordVertices(textureBoundsA); }, texCoordB: function (_, _a) { var textureBoundsB = _a.textureBoundsB; return getTexCoordVertices(textureBoundsB); } }) })); } var MAX_TEXTURE_DIMENSION = 1024; var TextureManager = (function () { function TextureManager(regl, tileSize) { var tilesAcross = Math.floor(MAX_TEXTURE_DIMENSION / tileSize); var pixelsAcross = tilesAcross * tileSize; var tileCapacity = tilesAcross * tilesAcross; var texture = regl.texture({ width: pixelsAcross, height: pixelsAcross, format: 'rgba', type: 'uint8', }); var contents = new Map(); var available = this.allTextureCoordinates(tilesAcross, tileSize); Object.assign(this, { tileSize: tileSize, tilesAcross: tilesAcross, pixelsAcross: pixelsAcross, tileCapacity: tileCapacity, texture: texture, contents: contents, available: available, }); } TextureManager.prototype.addTile = function (tileCoordinates, data) { var _a = this, available = _a.available, contents = _a.contents, texture = _a.texture, tileSize = _a.tileSize; var hashKey = this.hashTileCoordinates(tileCoordinates); if (contents.has(hashKey)) { var textureCoordinates_1 = contents.get(hashKey); contents.delete(hashKey); contents.set(hashKey, textureCoordinates_1); return this.formatOutputTextureCoordinates(textureCoordinates_1); } if (lodashEs.isEmpty(available)) { var firstInsertedKey = contents.keys().next().value; this.removeByHashKey(firstInsertedKey); } var textureCoordinates = available.pop(); contents.set(hashKey, textureCoordinates); var textureX = textureCoordinates.x, textureY = textureCoordinates.y; texture.subimage({ data: data, width: tileSize, height: tileSize, }, textureX, textureY); return this.formatOutputTextureCoordinates(textureCoordinates); }; TextureManager.prototype.removeTile = function (tileCoordinates) { this.removeByHashKey(this.hashTileCoordinates(tileCoordinates)); }; TextureManager.prototype.clearTiles = function () { for (var _i = 0, _a = Array.from(this.contents.keys()); _i < _a.length; _i++) { var hashKey = _a[_i]; this.removeByHashKey(hashKey); } }; TextureManager.prototype.destroy = function () { this.texture.destroy(); }; TextureManager.prototype.removeByHashKey = function (hashKey) { if (this.contents.has(hashKey)) { var textureCoordinates = this.contents.get(hashKey); this.contents.delete(hashKey); this.available.push(textureCoordinates); } }; TextureManager.prototype.formatOutputTextureCoordinates = function (textureCoordinates) { var x = textureCoordinates.x, y = textureCoordinates.y; var _a = this, pixelsAcross = _a.pixelsAcross, tileSize = _a.tileSize; return [ { x: x / pixelsAcross, y: y / pixelsAcross, }, { x: (x + tileSize) / pixelsAcross, y: (y + tileSize) / pixelsAcross, }, ]; }; TextureManager.prototype.hashTileCoordinates = function (_a) { var x = _a.x, y = _a.y, z = _a.z; return x + ":" + y + ":" + z; }; TextureManager.prototype.allTextureCoordinates = function (tilesAcross, tileSize) { return lodashEs.flatMap(range(tilesAcross), function (x) { return range(tilesAcross).map(function (y) { return ({ x: x * tileSize, y: y * tileSize, }); }); }); }; return TextureManager; }()); var Renderer = (function () { function Renderer(tileSize, nodataValue) { var canvas = L.DomUtil.create('canvas'); var regl = REGL(canvas); var commonDrawConfig = getCommonDrawConfiguration(tileSize, nodataValue); Object.assign(this, { canvas: canvas, regl: regl, tileSize: tileSize, textureManager: new TextureManager(regl, tileSize), drawTile: createDrawTileCommand(regl, commonDrawConfig), drawTileInterpolateColor: createDrawTileInterpolateColorCommand(regl, commonDrawConfig), drawTileInterpolateValue: createDrawTileInterpolateValueCommand(regl, commonDrawConfig), }); } Renderer.prototype.renderTile = function (_a, colorScale, sentinelValues) { var coords = _a.coords, pixelData = _a.pixelData; var _b = this, regl = _b.regl, textureManager = _b.textureManager, tileSize = _b.tileSize; this.setCanvasSize(tileSize, tileSize); var textureBounds = textureManager.addTile(coords, pixelData); regl.clear({ color: CLEAR_COLOR }); this.drawTile({ colorScale: convertColorScale(colorScale), sentinelValues: convertColorScale(sentinelValues), canvasSize: [tileSize, tileSize], canvasCoordinates: [0, 0], textureBounds: textureBounds, texture: textureManager.texture, }); return [0, 0]; }; Renderer.prototype.renderTiles = function (tiles, colorScale, sentinelValues) { var _a = this, regl = _a.regl, textureManager = _a.textureManager; var _b = this.computeRequiredCanvasDimensions(tiles.length), canvasWidth = _b[0], canvasHeight = _b[1]; this.setCanvasSize(canvasWidth, canvasHeight); var canvasCoordinates = this.getCanvasCoordinates(canvasWidth, canvasHeight, tiles.length); var tilesWithCanvasCoordinates = lodashEs.zipWith(tiles, canvasCoordinates, function (tile, canvasCoords) { return (__assign({}, tile, { canvasCoords: canvasCoords })); }); var webGLColorScale = convertColorScale(colorScale); var webGLSentinelValues = convertColorScale(sentinelValues); var canvasSize = [canvasWidth, canvasHeight]; textureManager.clearTiles(); regl.clear({ color: CLEAR_COLOR }); var chunks = lodashEs.chunk(tilesWithCanvasCoordinates, textureManager.tileCapacity); var _loop_1 = function (chunk_1) { var textureBounds = chunk_1.map(function (_a) { var coords = _a.coords, pixelData = _a.pixelData; return textureManager.addTile(coords, pixelData); }); this_1.drawTile(chunk_1.map(function (_a, index) { var canvasCoords = _a.canvasCoords; return ({ colorScale: webGLColorScale, sentinelValues: webGLSentinelValues, canvasSize: canvasSize, canvasCoordinates: canvasCoords, textureBounds: textureBounds[index], texture: textureManager.texture, }); })); }; var this_1 = this; for (var _i = 0, chunks_1 = chunks; _i < chunks_1.length; _i++) { var chunk_1 = chunks_1[_i]; _loop_1(chunk_1); } return canvasCoordinates; }; Renderer.prototype.renderTilesWithTransition = function (oldTiles, newTiles, colorScale, sentinelValues, transitionDurationMs, onFrameRendered) { return __awaiter(this, void 0, void 0, function () { var _a, regl, textureManager, tileSize, canvasSize, canvasWidth, canvasHeight, canvasCoordinates, tilesWithCanvasCoordinates, newTextureManager, webGLColorScale, webGLSentinelValues, transitionStart, renderFrame, animationHandle; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = this, regl = _a.regl, textureManager = _a.textureManager, tileSize = _a.tileSize; canvasSize = this.computeRequiredCanvasDimensions(oldTiles.length); canvasWidth = canvasSize[0], canvasHeight = canvasSize[1]; this.setCanvasSize(canvasWidth, canvasHeight); canvasCoordinates = this.getCanvasCoordinates(canvasWidth, canvasHeight, oldTiles.length); tilesWithCanvasCoordinates = lodashEs.zipWith(oldTiles, newTiles, canvasCoordinates, function (oldTile, newTile, canvasCoords) { return ({ coords: oldTile.coords, oldPixelData: oldTile.pixelData, newPixelData: newTile.pixelData, canvasCoords: canvasCoords, }); }); newTextureManager = new TextureManager(regl, tileSize); webGLColorScale = convertColorScale(colorScale); webGLSentinelValues = convertColorScale(sentinelValues); transitionStart = regl.now(); renderFrame = function (interpolationFraction) { var chunks = lodashEs.chunk(tilesWithCanvasCoordinates, textureManager.tileCapacity); regl.clear({ color: CLEAR_COLOR }); var _loop_2 = function (chunk_2) { var oldTextureBounds = chunk_2.map(function (_a) { var coords = _a.coords, oldPixelData = _a.oldPixelData; return textureManager.addTile(coords, oldPixelData); }); var newTextureBounds = chunk_2.map(function (_a) { var coords = _a.coords, newPixelData = _a.newPixelData; return newTextureManager.addTile(coords, newPixelData); }); _this.drawTileInterpolateValue(chunk_2.map(function (_a, index) { var canvasCoords = _a.canvasCoords; return ({ colorScale: webGLColorScale, sentinelValues: webGLSentinelValues, canvasSize: canvasSize, canvasCoordinates: canvasCoords, textureA: textureManager.texture, textureB: newTextureManager.texture, textureBoundsA: oldTextureBounds[index], textureBoundsB: newTextureBounds[index], interpolationFraction: interpolationFraction, }); })); }; for (var _i = 0, chunks_2 = chunks; _i < chunks_2.length; _i++) { var chunk_2 = chunks_2[_i]; _loop_2(chunk_2); } onFrameRendered(canvasCoordinates); }; animationHandle = regl.frame(function (_a) { var time = _a.time; var elapsedTimeMs = (time - transitionStart) * 1000; var interpolationFraction = elapsedTimeMs / transitionDurationMs; renderFrame(interpolationFraction); }); return [4, Timer(transitionDurationMs)]; case 1: _b.sent(); animationHandle.cancel(); renderFrame(1); this.textureManager.destroy(); this.textureManager = newTextureManager; return [2]; } }); }); }; Renderer.prototype.renderTilesWithTransitionAndNewColorScale = function (oldTiles, newTiles, oldColorScale, newColorScale, oldSentinelValues, newSentinelValues, transitionDurationMs, onFrameRendered) { return __awaiter(this, void 0, void 0, function () { var _a, regl, textureManager, tileSize, canvasSize, canvasWidth, canvasHeight, canvasCoordinates, tilesWithCanvasCoordinates, newTextureManager, colorScaleA, colorScaleB, sentinelValuesA, sentinelValuesB, transitionStart, renderFrame, animationHandle; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = this, regl = _a.regl, textureManager = _a.textureManager, tileSize = _a.tileSize; canvasSize = this.computeRequiredCanvasDimensions(oldTiles.length); canvasWidth = canvasSize[0], canvasHeight = canvasSize[1]; this.setCanvasSize(canvasWidth, canvasHeight); canvasCoordinates = this.getCanvasCoordinates(canvasWidth, canvasHeight, oldTiles.length); tilesWithCanvasCoordinates = lodashEs.zipWith(oldTiles, newTiles, canvasCoordinates, function (oldTile, newTile, canvasCoords) { return ({ coords: oldTile.coords, oldPixelData: oldTile.pixelData, newPixelData: newTile.pixelData, canvasCoords: canvasCoords, }); }); newTextureManager = new TextureManager(regl, tileSize); colorScaleA = convertColorScale(oldColorScale); colorScaleB = convertColorScale(newColorScale); sentinelValuesA = convertColorScale(oldSentinelValues); sentinelValuesB = convertColorScale(newSentinelValues); transitionStart = regl.now(); renderFrame = function (interpolationFraction) { var chunks = lodashEs.chunk(tilesWithCanvasCoordinates, textureMana