Skip to content

Commit af75416

Browse files
authored
fix: loadURLData throws if the url can not be resolved (#2586)
Add catching error if loadURLData is given a non existing file Replace loadFloat32Data by generic loadDataArray function Activate tests when loading from binary file. typedArray.ts: change args order for consistency
1 parent e7a1927 commit af75416

File tree

7 files changed

+159
-105
lines changed

7 files changed

+159
-105
lines changed

typescript/packages/subsurface-viewer/src/layers/grid3d/grid3dLayer.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type React from "react";
33

44
import type { Color } from "@deck.gl/core";
55
import { CompositeLayer } from "@deck.gl/core";
6-
import { JSONLoader, load } from "@loaders.gl/core";
76

87
import workerpool from "workerpool";
98

@@ -18,8 +17,8 @@ import type {
1817
import type { ColormapFunctionType } from "../utils/colormapTools";
1918

2019
import config from "../../SubsurfaceConfig.json";
21-
import { findConfig } from "../../utils/configTools";
2220
import type { BoundingBox3D } from "../../utils";
21+
import { findConfig, loadDataArray } from "../../utils";
2322

2423
// init workerpool
2524
const workerPoolConfig = findConfig(
@@ -78,26 +77,9 @@ async function loadData<T extends TTypedArray>(
7877
data: string | number[] | TTypedArray,
7978
type: { new (data: unknown): T }
8079
): Promise<T> {
81-
if (data instanceof type) {
82-
return data;
83-
}
84-
if (Array.isArray(data)) {
85-
return new type(data);
86-
}
87-
if (typeof data === "string") {
88-
const extension = data.split(".").pop()?.toLowerCase();
89-
// Data is a file name with .json extension
90-
if (extension === "json") {
91-
const stringData = await load(data, JSONLoader);
92-
return new type(stringData);
93-
}
94-
// It is assumed that the data is a file containing raw array of bytes.
95-
const response = await fetch(data);
96-
if (response.ok) {
97-
const blob = await response.blob();
98-
const buffer = await blob.arrayBuffer();
99-
return new type(buffer);
100-
}
80+
const result = await loadDataArray(data, type);
81+
if (result !== null) {
82+
return result;
10183
}
10284
return Promise.reject("Grid3DLayer: Unsupported type of input data");
10385
}
@@ -106,9 +88,13 @@ async function loadPropertiesData(
10688
propertiesData: string | number[] | Float32Array | Uint16Array
10789
): Promise<Float32Array | Uint16Array> {
10890
const isPropertiesDiscrete = propertiesData instanceof Uint16Array;
109-
return isPropertiesDiscrete
110-
? await loadData(propertiesData, Uint16Array)
111-
: await loadData(propertiesData, Float32Array);
91+
const result = isPropertiesDiscrete
92+
? await loadDataArray(propertiesData, Uint16Array)
93+
: await loadDataArray(propertiesData, Float32Array);
94+
if (result !== null) {
95+
return result;
96+
}
97+
return Promise.reject("Grid3DLayer: Unsupported type of input data");
11298
}
11399

114100
async function load_data(

typescript/packages/subsurface-viewer/src/layers/map/mapLayer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { getModelMatrix } from "../utils/layerTools";
1919
import type { ColormapFunctionType } from "../utils/colormapTools";
2020
import config from "../../SubsurfaceConfig.json";
2121
import { findConfig } from "../../utils/configTools";
22-
import { loadFloat32Data } from "../../utils/serialize";
22+
import { loadDataArray } from "../../utils/serialize";
2323
import PrivateMapLayer from "./privateMapLayer";
2424
import { rotate } from "./utils";
2525
import { makeFullMesh } from "./webworker";
@@ -92,8 +92,8 @@ async function loadMeshAndProperties(
9292
// Keep
9393
//const t0 = performance.now();
9494

95-
const mesh = await loadFloat32Data(meshData);
96-
const properties = await loadFloat32Data(propertiesData);
95+
const mesh = await loadDataArray(meshData, Float32Array);
96+
const properties = await loadDataArray(propertiesData, Float32Array);
9797

9898
// if (!isMesh && !isProperties) {
9999
// console.error("Error. One or both of texture and mesh must be given!");

typescript/packages/subsurface-viewer/src/utils/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export { add as addPoints3D } from "./Point3D";
66
export type { TypedArray, TypedFloatArray, TypedIntArray } from "./typedArray";
77
export { isNumberArray, isTypedArray, toTypedArray } from "./typedArray";
88

9+
export { loadDataArray } from "./serialize";
10+
911
export { proportionalZoom, scaleZoom } from "./camera";
1012

13+
export { findConfig } from "./configTools";
14+
1115
export { useScaleFactor } from "./event";

typescript/packages/subsurface-viewer/src/utils/serialize.test.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,41 @@ import "jest";
33
import fs from "fs";
44
import path from "path";
55

6-
import { loadFloat32Data } from "./serialize";
6+
import { loadDataArray } from "./serialize";
77

8-
const smallPropertyFile =
9-
"../../../../../example-data/small_properties.float32";
8+
const exampleDataDir = "../../../../../example-data";
9+
const smallPropertyFile = "small_properties.float32";
10+
11+
globalThis.fetch = jest.fn(async (url: string | URL | RequestInfo) => {
12+
if (typeof url === "string") {
13+
const filePath = path.resolve(__dirname, exampleDataDir, url);
14+
const buffer = fs.readFileSync(filePath);
15+
return {
16+
async blob() {
17+
return {
18+
async arrayBuffer() {
19+
return buffer.buffer.slice(
20+
buffer.byteOffset,
21+
buffer.byteOffset + buffer.byteLength
22+
);
23+
},
24+
};
25+
},
26+
headers: new Headers(),
27+
ok: true,
28+
status: 200,
29+
} as Response;
30+
}
31+
return {
32+
ok: false,
33+
status: 500,
34+
} as Response;
35+
});
1036

1137
// Helper to fetch and read the binary file as reference
1238
async function getReferenceFloat32Array(): Promise<Float32Array> {
1339
// Read the binary file using fs and assign it to Float32Array
14-
const filePath = path.resolve(__dirname, smallPropertyFile);
40+
const filePath = path.resolve(__dirname, exampleDataDir, smallPropertyFile);
1541
const buffer = fs.readFileSync(filePath);
1642
return new Float32Array(
1743
buffer.buffer,
@@ -25,29 +51,41 @@ async function getReferenceFloat32Array(): Promise<Float32Array> {
2551
// return new Float32Array(buffer);
2652
}
2753

28-
describe("loadFloat32Data", () => {
54+
describe("loadDataArray for Float32Array", () => {
2955
it("returns null for null input", async () => {
30-
const result = await loadFloat32Data(null as unknown as string);
56+
const result = await loadDataArray(
57+
null as unknown as string,
58+
Float32Array
59+
);
3160
expect(result).toBeNull();
3261
});
3362

3463
it("returns the same Float32Array if input is already Float32Array", async () => {
3564
const arr = new Float32Array([1, 2, 3]);
36-
const result = await loadFloat32Data(arr);
65+
const result = await loadDataArray(arr, Float32Array);
3766
expect(result).toBe(arr);
3867
});
3968

69+
it("converts Float64Array to Float32Array", async () => {
70+
const arr = new Float64Array([1, 2, 3]);
71+
const result = await loadDataArray(arr, Float32Array);
72+
expect(result!.length).toBe(arr.length);
73+
// Compare values with precision
74+
for (let i = 0; i < arr.length; i++) {
75+
expect(result![i]).toBeCloseTo(arr[i]);
76+
}
77+
});
78+
4079
it("converts number[] to Float32Array", async () => {
4180
const arr = [1, 2, 3];
42-
const result = await loadFloat32Data(arr);
81+
const result = await loadDataArray(arr, Float32Array);
4382
expect(result).toBeInstanceOf(Float32Array);
4483
expect(Array.from(result!)).toEqual(arr);
4584
});
4685

47-
// Needs elaborated mocking of fetch
48-
it.skip("loads Float32Array from a binary file URL", async () => {
86+
it("loads Float32Array from a binary file URL", async () => {
4987
const expected = await getReferenceFloat32Array();
50-
const result = await loadFloat32Data(smallPropertyFile);
88+
const result = await loadDataArray(smallPropertyFile, Float32Array);
5189
expect(result).toBeInstanceOf(Float32Array);
5290
expect(result!.length).toBe(expected.length);
5391
// Compare values with precision

typescript/packages/subsurface-viewer/src/utils/serialize.ts

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,103 @@
11
import * as png from "@vivaxy/png";
22

3+
import { JSONLoader, load } from "@loaders.gl/core";
4+
5+
import type { TConstructor, TypedArray } from "./typedArray";
6+
import { toTypedArray } from "./typedArray";
7+
8+
async function safeFetch(url: string): Promise<Response> {
9+
try {
10+
return await fetch(url);
11+
} catch (error) {
12+
console.error(`Failed to fetch ${url}:`, error);
13+
return new Response(null, { status: 500 });
14+
}
15+
}
16+
317
/**
4-
* Loads data from a URL as a Float32Array. Supports both PNG images (with absolute float values)
5-
* and binary float32 files. If the content type is 'image/png', the PNG is decoded and its pixel
6-
* data is returned as a Float32Array. Otherwise, the file is loaded as a binary array of floats.
18+
* Loads PNG image data (with absolute float values) from a Response object
19+
* and decodes it into a typed array of the specified type.
20+
*
21+
* This function reads the response as a Blob, decodes the PNG image data, and converts the pixel data
22+
* into a typed array (e.g., Float32Array, Uint8Array) using the provided constructor.
723
*
8-
* @param url - The URL to load data from
9-
* @returns A Promise resolving to a Float32Array with the data, or null if loading fails
24+
* @template T - The type of TypedArray to return (e.g., Float32Array, Uint8Array).
25+
* @param response - The Response object containing the PNG image data.
26+
* @param type - The constructor for the desired TypedArray type.
27+
* @returns A promise that resolves to a typed array containing the decoded image data, or null if decoding fails.
1028
*/
11-
export async function loadURLData(url: string): Promise<Float32Array | null> {
12-
let res: Float32Array | null = null;
13-
const response = await fetch(url);
14-
if (!response.ok) {
15-
console.error("Could not load ", url);
16-
}
29+
async function loadPngData<T extends TypedArray>(
30+
response: Response,
31+
type: TConstructor<T>
32+
): Promise<T | null> {
33+
// Load as PNG with absolute float values.
1734
const blob = await response.blob();
18-
const contentType = response.headers.get("content-type");
19-
const isPng = contentType === "image/png";
20-
if (isPng) {
21-
// Load as PNG with absolute float values.
22-
res = await new Promise((resolve) => {
23-
const fileReader = new FileReader();
24-
fileReader.readAsArrayBuffer(blob);
25-
fileReader.onload = () => {
26-
const arrayBuffer = fileReader.result;
27-
const imgData = png.decode(arrayBuffer as ArrayBuffer);
28-
const data = imgData.data; // array of ints (pixel data)
35+
const result: T | null = await new Promise((resolve) => {
36+
const fileReader = new FileReader();
37+
fileReader.readAsArrayBuffer(blob);
38+
fileReader.onload = () => {
39+
const arrayBuffer = fileReader.result;
40+
const imgData = png.decode(arrayBuffer as ArrayBuffer);
41+
const data = imgData.data; // array of ints (pixel data)
2942

30-
const n = data.length;
31-
const buffer = new ArrayBuffer(n);
32-
const view = new DataView(buffer);
33-
for (let i = 0; i < n; i++) {
34-
view.setUint8(i, data[i]);
35-
}
43+
const n = data.length;
44+
const buffer = new ArrayBuffer(n);
45+
const view = new DataView(buffer);
46+
for (let i = 0; i < n; i++) {
47+
view.setUint8(i, data[i]);
48+
}
3649

37-
const floatArray = new Float32Array(buffer);
38-
resolve(floatArray);
39-
};
40-
});
41-
} else {
42-
// Load as binary array of floats.
43-
const buffer = await blob.arrayBuffer();
44-
res = new Float32Array(buffer);
45-
}
46-
return res;
50+
const floatArray = new type(buffer);
51+
resolve(floatArray);
52+
};
53+
});
54+
return result;
55+
}
56+
57+
function isPngData(headers: Headers): boolean {
58+
const contentType = headers.get("content-type");
59+
return contentType === "image/png";
4760
}
4861

4962
/**
50-
* Loads data as a Float32Array from a string (URL), number array, or Float32Array.
51-
* If the input is a URL, it loads the data from the URL. If it's an array, it converts it.
63+
* Loads data as a typed array from a string (URL), number array, or any typed array.
64+
* If the input is a URL, it loads the data from the URL. Supports binary float32 files,
65+
* PNG images (with absolute float values) and json files storing number arrays.
5266
*
53-
* @param data - The data to load (string URL, number[], or Float32Array)
54-
* @returns A Promise resolving to a Float32Array or null if input is invalid
67+
* @param data - The data to load (string URL, base 64 encoded string, number[], or any typed array like Float32Array)
68+
* @param type - The targeted TypedArray type (e.g., any typed array like Float32Array)
69+
* @returns A Promise resolving to the targeted TypedArray or null if input is invalid
5570
*/
56-
export async function loadFloat32Data(
57-
data: string | number[] | Float32Array
58-
): Promise<Float32Array | null> {
71+
export async function loadDataArray<T extends TypedArray>(
72+
data: string | number[] | TypedArray,
73+
type: TConstructor<T>
74+
): Promise<T | null> {
5975
if (!data) {
6076
return null;
6177
}
62-
if (ArrayBuffer.isView(data)) {
63-
// Input data is typed array.
64-
return data;
65-
} else if (Array.isArray(data)) {
66-
// Input data is native javascript array.
67-
return new Float32Array(data);
78+
if (typeof data === "string") {
79+
const extension = data.split(".").pop()?.toLowerCase();
80+
// Data is a file name with .json extension
81+
if (extension === "json") {
82+
const stringData = await load(data, JSONLoader);
83+
return new type(stringData);
84+
}
85+
// It is assumed that the data is a file containing raw array of bytes.
86+
const response = await safeFetch(data);
87+
if (!response.ok) {
88+
return null;
89+
}
90+
if (isPngData(response.headers)) {
91+
return await loadPngData(response, type);
92+
}
93+
94+
const blob = await response.blob();
95+
const buffer = await blob.arrayBuffer();
96+
return new type(buffer);
6897
} else {
69-
// Input data is an URL.
70-
return await loadURLData(data);
98+
return toTypedArray(data, type);
7199
}
100+
return Promise.reject("loadDataArray: unsupported type of input data");
72101
}
73102

74103
/**

typescript/packages/subsurface-viewer/src/utils/typedArray.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,34 +71,34 @@ describe("Test isTypedArray", () => {
7171
describe("Test toTypedArray", () => {
7272
it("should convert a number[] to the specified TypedArray", () => {
7373
const arr = [1, 2, 3];
74-
const result = toTypedArray(Float64Array, arr);
74+
const result = toTypedArray(arr, Float64Array);
7575
expect(result).toBeInstanceOf(Float64Array);
7676
expect(Array.from(result)).toEqual(arr);
7777
});
7878

7979
it("should return the same TypedArray if input is already a TypedArray", () => {
8080
const arr = new Int16Array([4, 5, 6]);
81-
const result = toTypedArray(Int16Array, arr);
81+
const result = toTypedArray(arr, Int16Array);
8282
expect(result).toBe(arr);
8383
});
8484

8585
it("should convert a Float32Array to Int8Array", () => {
8686
const arr = new Float32Array([1.7, -2.2, 3.9]);
87-
const result = toTypedArray(Int8Array, arr);
87+
const result = toTypedArray(arr, Int8Array);
8888
expect(result).toBeInstanceOf(Int8Array);
8989
expect(Array.from(result)).toEqual([1, -2, 3]);
9090
});
9191

9292
it("should convert a Uint8Array to Float32Array", () => {
9393
const arr = new Uint8Array([10, 20, 30]);
94-
const result = toTypedArray(Float32Array, arr);
94+
const result = toTypedArray(arr, Float32Array);
9595
expect(result).toBeInstanceOf(Float32Array);
9696
expect(Array.from(result)).toEqual([10, 20, 30]);
9797
});
9898

9999
it("should handle empty arrays", () => {
100100
const arr: number[] = [];
101-
const result = toTypedArray(Uint16Array, arr);
101+
const result = toTypedArray(arr, Uint16Array);
102102
expect(result).toBeInstanceOf(Uint16Array);
103103
expect(result.length).toBe(0);
104104
});

0 commit comments

Comments
 (0)