layer/geotiff/ColorScale.tstypescript
import type { ColorScaleName, ColorStop, IMultiBandRenderOptions, ISingleBandRenderOptions } from "./types";
export type TypedRasterArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| number[];
interface ColorScaleDefinition {
colors: string[] | Uint8Array;
positions?: number[];
}
export const COLORSCALES: Record<string, ColorScaleDefinition> = {
viridis: {
colors: new Uint8Array([
68, 1, 84, 255, 68, 2, 86, 255, 69, 4, 87, 255, 69, 5, 89, 255, 70, 7, 90, 255, 70, 8, 92, 255, 70, 10, 93,
255, 70, 11, 94, 255, 71, 13, 96, 255, 71, 14, 97, 255, 71, 16, 99, 255, 71, 17, 100, 255, 71, 19, 101, 255,
72, 20, 103, 255, 72, 22, 104, 255, 72, 23, 105, 255, 72, 24, 106, 255, 72, 26, 108, 255, 72, 27, 109, 255,
72, 28, 110, 255, 72, 29, 111, 255, 72, 31, 112, 255, 72, 32, 113, 255, 72, 33, 115, 255, 72, 35, 116, 255,
72, 36, 117, 255, 72, 37, 118, 255, 72, 38, 119, 255, 72, 40, 120, 255, 72, 41, 121, 255, 71, 42, 122, 255,
71, 44, 122, 255, 71, 45, 123, 255, 71, 46, 124, 255, 71, 47, 125, 255, 70, 48, 126, 255, 70, 50, 126, 255,
70, 51, 127, 255, 70, 52, 128, 255, 69, 53, 129, 255, 69, 55, 129, 255, 69, 56, 130, 255, 68, 57, 131, 255,
68, 58, 131, 255, 68, 59, 132, 255, 67, 61, 132, 255, 67, 62, 133, 255, 66, 63, 133, 255, 66, 64, 134, 255,
66, 65, 134, 255, 65, 66, 135, 255, 65, 68, 135, 255, 64, 69, 136, 255, 64, 70, 136, 255, 63, 71, 136, 255,
63, 72, 137, 255, 62, 73, 137, 255, 62, 74, 137, 255, 62, 76, 138, 255, 61, 77, 138, 255, 61, 78, 138, 255,
60, 79, 138, 255, 60, 80, 139, 255, 59, 81, 139, 255, 59, 82, 139, 255, 58, 83, 139, 255, 58, 84, 140, 255,
57, 85, 140, 255, 57, 86, 140, 255, 56, 88, 140, 255, 56, 89, 140, 255, 55, 90, 140, 255, 55, 91, 141, 255,
54, 92, 141, 255, 54, 93, 141, 255, 53, 94, 141, 255, 53, 95, 141, 255, 52, 96, 141, 255, 52, 97, 141, 255,
51, 98, 141, 255, 51, 99, 141, 255, 50, 100, 142, 255, 50, 101, 142, 255, 49, 102, 142, 255, 49, 103, 142,
255, 49, 104, 142, 255, 48, 105, 142, 255, 48, 106, 142, 255, 47, 107, 142, 255, 47, 108, 142, 255, 46, 109,
142, 255, 46, 110, 142, 255, 46, 111, 142, 255, 45, 112, 142, 255, 45, 113, 142, 255, 44, 113, 142, 255, 44,
114, 142, 255, 44, 115, 142, 255, 43, 116, 142, 255, 43, 117, 142, 255, 42, 118, 142, 255, 42, 119, 142,
255, 42, 120, 142, 255, 41, 121, 142, 255, 41, 122, 142, 255, 41, 123, 142, 255, 40, 124, 142, 255, 40, 125,
142, 255, 39, 126, 142, 255, 39, 127, 142, 255, 39, 128, 142, 255, 38, 129, 142, 255, 38, 130, 142, 255, 38,
130, 142, 255, 37, 131, 142, 255, 37, 132, 142, 255, 37, 133, 142, 255, 36, 134, 142, 255, 36, 135, 142,
255, 35, 136, 142, 255, 35, 137, 142, 255, 35, 138, 141, 255, 34, 139, 141, 255, 34, 140, 141, 255, 34, 141,
141, 255, 33, 142, 141, 255, 33, 143, 141, 255, 33, 144, 141, 255, 33, 145, 140, 255, 32, 146, 140, 255, 32,
146, 140, 255, 32, 147, 140, 255, 31, 148, 140, 255, 31, 149, 139, 255, 31, 150, 139, 255, 31, 151, 139,
255, 31, 152, 139, 255, 31, 153, 138, 255, 31, 154, 138, 255, 30, 155, 138, 255, 30, 156, 137, 255, 30, 157,
137, 255, 31, 158, 137, 255, 31, 159, 136, 255, 31, 160, 136, 255, 31, 161, 136, 255, 31, 161, 135, 255, 31,
162, 135, 255, 32, 163, 134, 255, 32, 164, 134, 255, 33, 165, 133, 255, 33, 166, 133, 255, 34, 167, 133,
255, 34, 168, 132, 255, 35, 169, 131, 255, 36, 170, 131, 255, 37, 171, 130, 255, 37, 172, 130, 255, 38, 173,
129, 255, 39, 173, 129, 255, 40, 174, 128, 255, 41, 175, 127, 255, 42, 176, 127, 255, 44, 177, 126, 255, 45,
178, 125, 255, 46, 179, 124, 255, 47, 180, 124, 255, 49, 181, 123, 255, 50, 182, 122, 255, 52, 182, 121,
255, 53, 183, 121, 255, 55, 184, 120, 255, 56, 185, 119, 255, 58, 186, 118, 255, 59, 187, 117, 255, 61, 188,
116, 255, 63, 188, 115, 255, 64, 189, 114, 255, 66, 190, 113, 255, 68, 191, 112, 255, 70, 192, 111, 255, 72,
193, 110, 255, 74, 193, 109, 255, 76, 194, 108, 255, 78, 195, 107, 255, 80, 196, 106, 255, 82, 197, 105,
255, 84, 197, 104, 255, 86, 198, 103, 255, 88, 199, 101, 255, 90, 200, 100, 255, 92, 200, 99, 255, 94, 201,
98, 255, 96, 202, 96, 255, 99, 203, 95, 255, 101, 203, 94, 255, 103, 204, 92, 255, 105, 205, 91, 255, 108,
205, 90, 255, 110, 206, 88, 255, 112, 207, 87, 255, 115, 208, 86, 255, 117, 208, 84, 255, 119, 209, 83, 255,
122, 209, 81, 255, 124, 210, 80, 255, 127, 211, 78, 255, 129, 211, 77, 255, 132, 212, 75, 255, 134, 213, 73,
255, 137, 213, 72, 255, 139, 214, 70, 255, 142, 214, 69, 255, 144, 215, 67, 255, 147, 215, 65, 255, 149,
216, 64, 255, 152, 216, 62, 255, 155, 217, 60, 255, 157, 217, 59, 255, 160, 218, 57, 255, 162, 218, 55, 255,
165, 219, 54, 255, 168, 219, 52, 255, 170, 220, 50, 255, 173, 220, 48, 255, 176, 221, 47, 255, 178, 221, 45,
255, 181, 222, 43, 255, 184, 222, 41, 255, 186, 222, 40, 255, 189, 223, 38, 255, 192, 223, 37, 255, 194,
223, 35, 255, 197, 224, 33, 255, 200, 224, 32, 255, 202, 225, 31, 255, 205, 225, 29, 255, 208, 225, 28, 255,
210, 226, 27, 255, 213, 226, 26, 255, 216, 226, 25, 255, 218, 227, 25, 255, 221, 227, 24, 255, 223, 227, 24,
255, 226, 228, 24, 255, 229, 228, 25, 255, 231, 228, 25, 255, 234, 229, 26, 255, 236, 229, 27, 255, 239,
229, 28, 255, 241, 229, 29, 255, 244, 230, 30, 255, 246, 230, 32, 255, 248, 230, 33, 255, 251, 231, 35, 255,
253, 231, 37, 255
])
},
turbo: {
colors: [
"#30123b",
"#321543",
"#33184a",
"#341b51",
"#351e58",
"#36215f",
"#372466",
"#38276d",
"#392a73",
"#3a2d79",
"#3b2f80",
"#3c3286",
"#3d358b",
"#3e3891",
"#3f3b97",
"#3f3e9c",
"#4040a2",
"#4143a7",
"#4146ac",
"#4249b1",
"#424bb5",
"#434eba",
"#4451bf",
"#4454c3",
"#4456c7",
"#4559cb",
"#455ccf",
"#455ed3",
"#4661d6",
"#4664da",
"#4666dd",
"#4669e0",
"#466be3",
"#476ee6",
"#4771e9",
"#4773eb",
"#4776ee",
"#4778f0",
"#477bf2",
"#467df4",
"#4680f6",
"#4682f8",
"#4685fa",
"#4687fb",
"#458afc",
"#458cfd",
"#448ffe",
"#4391fe",
"#4294ff",
"#4196ff",
"#4099ff",
"#3e9bfe",
"#3d9efe",
"#3ba0fd",
"#3aa3fc",
"#38a5fb",
"#37a8fa",
"#35aaf8",
"#33acf7",
"#31aff5",
"#2fb1f4",
"#2eb3f2",
"#2cb6f0",
"#2ab8ee",
"#28baeb",
"#27bde9",
"#25bfe7",
"#23c1e4",
"#22c3e2",
"#20c5df",
"#1fc7dd",
"#1ec9da",
"#1ccbd8",
"#1bced5",
"#1ad0d2",
"#19d2d0",
"#18d4cd",
"#18d5cb",
"#17d7c8",
"#17d9c5",
"#17dbc2",
"#17ddc0",
"#17dfbd",
"#17e0ba",
"#18e2b8",
"#19e3b5",
"#1ae4b2",
"#1ce6af",
"#1de7ac",
"#1fe8a9",
"#20e9a6",
"#22eba3",
"#25eca0",
"#27eda0",
"#2aeea0",
"#2ceea0",
"#2feea0",
"#32eea0",
"#35efa0",
"#38efa0",
"#3befa0",
"#3eefa0",
"#42eea0",
"#45eea0",
"#48eea0",
"#4beea0",
"#4eeea0",
"#51eda0",
"#55eda0",
"#58eda0",
"#5beca0",
"#5eeca0",
"#62eca0",
"#65eba0",
"#69eba0",
"#6ceaa0",
"#70eaa0",
"#73e9a0",
"#77e8a0",
"#7ae8a0",
"#7ee7a0",
"#81e6a0",
"#85e5a0",
"#89e4a0",
"#8ce3a0",
"#90e2a0",
"#94e1a0",
"#97e0a0",
"#9bdfa0",
"#9fdda0",
"#a2dca0",
"#a6dba0",
"#a9d9a0",
"#add8a0",
"#b0d6a0",
"#b4d5a0",
"#b7d3a0",
"#bbd2a0",
"#bed0a0",
"#c1cea0",
"#c5cda0",
"#c8cba0",
"#cbc9a0",
"#cec7a0",
"#d1c5a0",
"#d4c3a0",
"#d7c1a0",
"#d9bfa0",
"#dcbea0",
"#dfbca0",
"#e1baa0",
"#e4b8a0",
"#e6b6a0",
"#e8b4a0",
"#ebb2a0",
"#edb0a0",
"#efaea0",
"#f1aca0",
"#f3aaa0",
"#f5a8a0",
"#f6a6a0",
"#f8a4a0",
"#f9a2a0",
"#fba0a0",
"#fc9ea0",
"#fd9ca0",
"#fe9aa0",
"#ff98a0",
"#ff96a0",
"#ff94a0"
]
},
inferno: {
colors: [
"#000004",
"#010005",
"#010106",
"#010108",
"#02010a",
"#02020c",
"#02020e",
"#030210",
"#040312",
"#040314",
"#050417",
"#060419",
"#07051b",
"#08051d",
"#09061f",
"#0a0722",
"#0b0724",
"#0c0826",
"#0d0829",
"#0e092b",
"#10092d",
"#110a30",
"#120a32",
"#140b34",
"#150b37",
"#160b39",
"#180c3c",
"#190c3e",
"#1b0c41",
"#1c0c43",
"#1e0c45",
"#1f0c48",
"#210c4a",
"#230c4c",
"#240c4f",
"#260c51",
"#280b53",
"#290b55",
"#2b0b57",
"#2d0b59",
"#2f0a5b",
"#310a5c",
"#320a5e",
"#340a5f",
"#360961",
"#380962",
"#390963",
"#3b0964",
"#3d0965",
"#3e0966",
"#400a67",
"#420a68",
"#440a68",
"#450a69",
"#470b6a",
"#490b6a",
"#4a0c6b",
"#4c0c6b",
"#4d0d6c",
"#4f0d6c",
"#510e6c",
"#520e6d",
"#540f6d",
"#550f6d",
"#57106e",
"#59106e",
"#5a116e",
"#5c126e",
"#5d126e",
"#5f136e",
"#61136e",
"#62146e",
"#64156e",
"#65156e",
"#67166e",
"#69166e",
"#6a176e",
"#6c186e",
"#6d186e",
"#6f196e",
"#71196e",
"#721a6e",
"#741a6e",
"#751b6e",
"#771c6d",
"#781c6d",
"#7a1d6d",
"#7c1d6d",
"#7d1e6d",
"#7f1e6c",
"#801f6c",
"#82206c",
"#84206b",
"#85216b",
"#87216b",
"#88226a",
"#8a226a",
"#8c2369",
"#8d2369",
"#8f2469",
"#902568",
"#922568",
"#932667",
"#952667",
"#972766",
"#982766",
"#9a2865",
"#9b2964",
"#9d2964",
"#9f2a63",
"#a02a63",
"#a22b62",
"#a32c61",
"#a52c60",
"#a62d60",
"#a82e5f",
"#a92e5e",
"#ab2f5e",
"#ad305d",
"#ae305c",
"#b0315b",
"#b1325a",
"#b3325a",
"#b43359",
"#b63458",
"#b73557",
"#b93556",
"#ba3655",
"#bc3754",
"#bd3853",
"#bf3952",
"#c03a51",
"#c13a50",
"#c33b4f",
"#c43c4e",
"#c63d4d",
"#c73e4c",
"#c83f4b",
"#ca404a",
"#cb4149",
"#cc4248",
"#ce4347",
"#cf4446",
"#d04545",
"#d24644",
"#d34743",
"#d44842",
"#d54a41",
"#d74b3f",
"#d84c3e",
"#d94d3d",
"#da4e3c",
"#db503b",
"#dc513a",
"#de5238",
"#df5337",
"#e05536",
"#e15635",
"#e25734",
"#e35933",
"#e45a31",
"#e55c30",
"#e65d2f",
"#e75e2e",
"#e8602d",
"#e9612b",
"#ea632a",
"#eb6429",
"#eb6628",
"#ec6726",
"#ed6925",
"#ee6a24",
"#ef6c23",
"#ef6e21",
"#f06f20",
"#f1711f",
"#f1731d",
"#f2741c",
"#f3761b",
"#f37819",
"#f47918",
"#f57b17",
"#f57d15",
"#f67e14",
"#f68013",
"#f78212",
"#f78410",
"#f8850f",
"#f8870e",
"#f8890c",
"#f98b0b",
"#f98c0a",
"#f98e09",
"#fa9008",
"#fa9207",
"#fa9407",
"#fb9606",
"#fb9706",
"#fb9906",
"#fb9b06",
"#fb9d07",
"#fc9f07",
"#fca108",
"#fca309",
"#fca50a",
"#fca60c",
"#fca80d",
"#fcaa0f",
"#fcac11",
"#fcae12",
"#fcb014",
"#fcb216",
"#fcb418",
"#fbb61a",
"#fbb81d",
"#fbbb1f",
"#fbbd21",
"#fbbf23",
"#fbc126",
"#fac328",
"#fac52b",
"#fac72d",
"#f9c930",
"#f9cb33",
"#f8cd35",
"#f8cf38",
"#f7d13b",
"#f7d33e",
"#f6d541",
"#f6d744",
"#f5d947",
"#f5db4a",
"#f4dd4d",
"#f4df51",
"#f3e154",
"#f3e357",
"#f3e55a",
"#f2e75e",
"#f2e961",
"#f1eb65",
"#f1ed68",
"#f1ef6c",
"#f1f170",
"#f2f274",
"#f2f479",
"#f3f67d",
"#f4f882",
"#f5f986",
"#f6fa8b",
"#f8fb90"
]
},
rainbow: {
colors: ["#96005A", "#0000C8", "#0019FF", "#0098FF", "#2CFF96", "#97FF00", "#FFEA00", "#FF6F00", "#FF0000"],
positions: [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]
},
jet: {
colors: ["#000083", "#003CAA", "#05FFFF", "#FFFF00", "#FA0000", "#800000"],
positions: [0, 0.125, 0.375, 0.625, 0.875, 1]
},
greys: {
colors: ["#000000", "#ffffff"],
positions: [0, 1]
},
earth: {
colors: ["#000082", "#00b4b4", "#28d228", "#e6e632", "#784614", "#ffffff"],
positions: [0, 0.1, 0.2, 0.4, 0.6, 1]
},
coolwarm: {
colors: ["#0000ff", "#ffffff", "#ff0000"],
positions: [0, 0.5, 1]
},
hot: {
colors: ["#000000", "#e60000", "#ffd200", "#ffffff"],
positions: [0, 0.3, 0.6, 1]
},
cool: {
colors: ["#00ffff", "#ff00ff"],
positions: [0, 1]
}
};
/**
* Parses a hex, rgb, or named color string into RGBA [0..255] values.
*/
export function parseColor(color: string): [number, number, number, number] {
const trimmed = color.trim().toLowerCase();
if (trimmed.startsWith("#")) {
const hex = trimmed.slice(1);
if (hex.length === 3) {
return [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16), 255];
}
if (hex.length === 6) {
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), 255];
}
if (hex.length === 8) {
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
parseInt(hex.slice(6, 8), 16)
];
}
}
if (trimmed.startsWith("rgb")) {
const parts = trimmed
.replace(/[rgba()]/g, "")
.split(",")
.map((s) => parseFloat(s.trim()));
return [
Math.round(parts[0] || 0),
Math.round(parts[1] || 0),
Math.round(parts[2] || 0),
parts.length > 3 ? Math.round((parts[3] ?? 1) * 255) : 255
];
}
// Default fallback (black)
return [0, 0, 0, 255];
}
/**
* Builds a 256-color RGBA Lookup Table (Uint8Array of size 1024) from render options.
*/
export function buildColorLUT(options?: ISingleBandRenderOptions): Uint8Array {
const lut = new Uint8Array(256 * 4);
let stops: { pos: number; color: [number, number, number, number] }[] = [];
if (options?.colors && options.colors.length > 0) {
if (Array.isArray(options.colors[0])) {
const arr = options.colors as ColorStop[];
stops = arr.map(([pos, col]) => ({
pos: Math.max(0, Math.min(1, pos)),
color: parseColor(col)
}));
} else {
const colList = options.colors as string[];
const count = colList.length;
stops = colList.map((col, idx) => ({
pos: count > 1 ? idx / (count - 1) : 0,
color: parseColor(col)
}));
}
stops.sort((a, b) => a.pos - b.pos);
} else {
const scaleName = (options?.colorScale || "viridis").toLowerCase();
const def = COLORSCALES[scaleName] || COLORSCALES.viridis;
if (def.colors instanceof Uint8Array) {
// Already a precomputed 256*4 table
if (def.colors.length === 256 * 4) {
lut.set(def.colors);
return lut;
}
}
const colList = def.colors as string[];
const positions = def.positions || colList.map((_, i) => i / Math.max(1, colList.length - 1));
stops = colList.map((col, idx) => ({
pos: positions[idx] ?? idx / (colList.length - 1),
color: parseColor(col)
}));
}
if (stops.length === 0) {
stops = [
{ pos: 0, color: [0, 0, 0, 255] },
{ pos: 1, color: [255, 255, 255, 255] }
];
}
// Interpolate 256 entries
for (let i = 0; i < 256; i++) {
const t = i / 255;
let c0 = stops[0];
let c1 = stops[stops.length - 1];
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].pos && t <= stops[j + 1].pos) {
c0 = stops[j];
c1 = stops[j + 1];
break;
}
}
const range = c1.pos - c0.pos;
const localT = range > 0 ? (t - c0.pos) / range : 0;
const r = Math.round(c0.color[0] + (c1.color[0] - c0.color[0]) * localT);
const g = Math.round(c0.color[1] + (c1.color[1] - c0.color[1]) * localT);
const b = Math.round(c0.color[2] + (c1.color[2] - c0.color[2]) * localT);
const a = Math.round(c0.color[3] + (c1.color[3] - c0.color[3]) * localT);
const offset = i * 4;
lut[offset] = r;
lut[offset + 1] = g;
lut[offset + 2] = b;
lut[offset + 3] = a;
}
return lut;
}
/**
* Parses and normalizes a nodata value from various representations (number, null, string "NaN", NaN, etc.).
*/
export function parseNoDataValue(val: any): number | null | undefined {
if (val === undefined) return undefined;
if (val === null) return null;
if (typeof val === "string") {
const lower = val.trim().toLowerCase();
if (lower === "nan" || lower === "-nan" || lower === "+nan") return NaN;
if (lower === "none" || lower === "null" || lower === "") return null;
const parsed = Number(val);
return Number.isNaN(parsed) ? null : parsed;
}
if (typeof val === "number") {
return val;
}
return undefined;
}
/**
* Checks if a raster value represents NoData, taking NaN, null, and float precision into account.
*/
export function isNoData(val: number, nodata: number | null | undefined): boolean {
if (Number.isNaN(val)) return true;
if (nodata === null || nodata === undefined || Number.isNaN(nodata)) return false;
if (val === nodata) return true;
if (Math.fround(val) === Math.fround(nodata)) return true;
const absDiff = Math.abs(val - nodata);
if (absDiff < 1e-6) return true;
const maxVal = Math.max(Math.abs(val), Math.abs(nodata));
if (maxVal > 0 && absDiff / maxVal < 1e-5) return true;
return false;
}
/**
* Returns a high-performance nodata checking predicate for inner pixel loops.
*/
export function getFastNoDataChecker(nodata: number | null | undefined): (val: number) => boolean {
if (nodata === null || nodata === undefined || Number.isNaN(nodata)) {
return (val: number) => Number.isNaN(val);
}
const fNodata = Math.fround(nodata);
return (val: number) => {
if (val === nodata || Number.isNaN(val)) return true;
if (Math.fround(val) === fNodata) return true;
const absDiff = Math.abs(val - nodata);
if (absDiff < 1e-6) return true;
const maxVal = Math.max(Math.abs(val), Math.abs(nodata));
return maxVal > 0 && absDiff / maxVal < 1e-5;
};
}
/**
* Calculates min and max from a raster array, ignoring NaN and NoData.
*/
export function getRasterMinMax(
data: TypedRasterArray,
nodata: number | null | undefined
): { min: number; max: number } {
let min = Infinity;
let max = -Infinity;
const len = data.length;
for (let i = 0; i < len; i++) {
const val = data[i];
if (isNoData(val, nodata)) {
continue;
}
if (val < min) min = val;
if (val > max) max = val;
}
if (min === Infinity || max === -Infinity) {
return { min: 0, max: 1 };
}
return { min, max };
}
/**
* Creates an ImageData instance with fallback for non-browser/worker/test environments.
*/
export function createImageData(width: number, height: number): ImageData {
if (typeof ImageData !== "undefined") {
return new ImageData(width, height);
}
return {
width,
height,
data: new Uint8ClampedArray(width * height * 4),
colorSpace: "srgb"
} as unknown as ImageData;
}
/**
* Renders a single band raster to an ImageData using a color LUT.
*/
export function renderSingleBandToImageData(
raster: TypedRasterArray,
width: number,
height: number,
options?: ISingleBandRenderOptions,
nodata: number | null = null,
outImageData?: ImageData
): ImageData {
const imgData = outImageData || createImageData(width, height);
const rgba = imgData.data;
const lut = buildColorLUT(options);
let [min, max] = options?.domain || [undefined, undefined];
if (min === undefined || max === undefined) {
const minMax = getRasterMinMax(raster, nodata);
min = min ?? minMax.min;
max = max ?? minMax.max;
}
const diff = max - min || 1e-6;
const scale = 255 / diff;
const displayRange = options?.displayRange;
const clampLow = options?.clampLow ?? true;
const clampHigh = options?.clampHigh ?? true;
const pixelCount = width * height;
const hasNoData = nodata !== null && nodata !== undefined && !Number.isNaN(nodata);
const checkNoData = hasNoData ? getFastNoDataChecker(nodata) : null;
for (let i = 0; i < pixelCount; i++) {
const val = raster[i];
const outIdx = i * 4;
if (checkNoData ? checkNoData(val) : Number.isNaN(val)) {
rgba[outIdx + 3] = 0;
continue;
}
if (displayRange && (val < displayRange[0] || val > displayRange[1])) {
rgba[outIdx + 3] = 0;
continue;
}
let norm255 = ((val - min) * scale + 0.5) | 0;
if (clampLow && norm255 < 0) norm255 = 0;
if (clampHigh && norm255 > 255) norm255 = 255;
if (norm255 < 0 || norm255 > 255) {
rgba[outIdx + 3] = 0;
continue;
}
const lutIdx = norm255 * 4;
rgba[outIdx] = lut[lutIdx];
rgba[outIdx + 1] = lut[lutIdx + 1];
rgba[outIdx + 2] = lut[lutIdx + 2];
rgba[outIdx + 3] = lut[lutIdx + 3];
}
return imgData;
}
/**
* Renders 3 separate band arrays into an RGB ImageData.
*/
export function renderMultiBandToImageData(
rBand: TypedRasterArray,
gBand: TypedRasterArray,
bBand: TypedRasterArray,
width: number,
height: number,
options?: IMultiBandRenderOptions,
nodata: number | null = null,
outImageData?: ImageData
): ImageData {
const imgData = outImageData || createImageData(width, height);
const rgba = imgData.data;
const pixelCount = width * height;
const rMin = options?.r?.min ?? 0;
const rMax = options?.r?.max ?? 255;
const rDiff = rMax - rMin || 1;
const gMin = options?.g?.min ?? 0;
const gMax = options?.g?.max ?? 255;
const gDiff = gMax - gMin || 1;
const bMin = options?.b?.min ?? 0;
const bMax = options?.b?.max ?? 255;
const bDiff = bMax - bMin || 1;
const rScale = 255 / rDiff;
const gScale = 255 / gDiff;
const bScale = 255 / bDiff;
const hasNoData = nodata !== null && nodata !== undefined && !Number.isNaN(nodata);
if (!hasNoData) {
for (let i = 0; i < pixelCount; i++) {
const outIdx = i * 4;
const rVal = rBand[i];
const gVal = gBand[i];
const bVal = bBand[i];
if (Number.isNaN(rVal) || Number.isNaN(gVal) || Number.isNaN(bVal)) {
rgba[outIdx + 3] = 0;
continue;
}
let r = ((rVal - rMin) * rScale + 0.5) | 0;
if (r < 0) r = 0;
else if (r > 255) r = 255;
let g = ((gVal - gMin) * gScale + 0.5) | 0;
if (g < 0) g = 0;
else if (g > 255) g = 255;
let b = ((bVal - bMin) * bScale + 0.5) | 0;
if (b < 0) b = 0;
else if (b > 255) b = 255;
rgba[outIdx] = r;
rgba[outIdx + 1] = g;
rgba[outIdx + 2] = b;
rgba[outIdx + 3] = 255;
}
} else {
const checkNoData = getFastNoDataChecker(nodata);
for (let i = 0; i < pixelCount; i++) {
const outIdx = i * 4;
const rVal = rBand[i];
const gVal = gBand[i];
const bVal = bBand[i];
if (checkNoData(rVal) || checkNoData(gVal) || checkNoData(bVal)) {
rgba[outIdx + 3] = 0;
continue;
}
let r = ((rVal - rMin) * rScale + 0.5) | 0;
if (r < 0) r = 0;
else if (r > 255) r = 255;
let g = ((gVal - gMin) * gScale + 0.5) | 0;
if (g < 0) g = 0;
else if (g > 255) g = 255;
let b = ((bVal - bMin) * bScale + 0.5) | 0;
if (b < 0) b = 0;
else if (b > 255) b = 255;
rgba[outIdx] = r;
rgba[outIdx + 1] = g;
rgba[outIdx + 2] = b;
rgba[outIdx + 3] = 255;
}
}
return imgData;
}
/**
* Converts raw RGB/RGBA raster array(s) directly to an ImageData.
*/
export function renderRgbRastersToImageData(
rasters: TypedRasterArray[],
width: number,
height: number,
nodata: number | null = null,
outImageData?: ImageData
): ImageData {
const imgData = outImageData || createImageData(width, height);
const rgba = imgData.data;
const pixelCount = width * height;
const isRgba = rasters.length >= 4;
const rBand = rasters[0];
const gBand = rasters[1] || rasters[0];
const bBand = rasters[2] || rasters[0];
const aBand = isRgba ? rasters[3] : null;
const hasNoData = nodata !== null && nodata !== undefined && !Number.isNaN(nodata);
if (!hasNoData) {
for (let i = 0; i < pixelCount; i++) {
const outIdx = i * 4;
const r = rBand[i];
const g = gBand[i];
const b = bBand[i];
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
rgba[outIdx + 3] = 0;
continue;
}
rgba[outIdx] = r;
rgba[outIdx + 1] = g;
rgba[outIdx + 2] = b;
rgba[outIdx + 3] = isRgba ? aBand![i] : 255;
}
} else {
const checkNoData = getFastNoDataChecker(nodata);
for (let i = 0; i < pixelCount; i++) {
const outIdx = i * 4;
const r = rBand[i];
const g = gBand[i];
const b = bBand[i];
const a = aBand ? aBand[i] : 255;
if (checkNoData(r) || checkNoData(g) || checkNoData(b)) {
rgba[outIdx + 3] = 0;
continue;
}
rgba[outIdx] = r;
rgba[outIdx + 1] = g;
rgba[outIdx + 2] = b;
rgba[outIdx + 3] = a;
}
}
return imgData;
}