UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

384 lines (341 loc) 12.7 kB
import '../../Common/DataModel/ImageData.js'; import '../../Common/DataModel/PolyData.js'; import vtk from '../../vtk.js'; import { m as macro } from '../../macros2.js'; import DataAccessHelper from './DataAccessHelper.js'; import vtkDataArray from '../../Common/Core/DataArray.js'; import vtkStringArray from '../../Common/Core/StringArray.js'; import './DataAccessHelper/LiteHttpDataAccessHelper.js'; // For vtk factory // import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip // import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip // import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip const fieldDataLocations = ['pointData', 'cellData', 'fieldData']; const ARRAY_BUILDERS = { vtkDataArray, vtkStringArray }; // ---------------------------------------------------------------------------- // Global methods // ---------------------------------------------------------------------------- const cachedArraysAndPromises = {}; const cachedArraysMetaData = {}; const MiB = 1024 * 1024; const GEOMETRY_ARRAYS = { vtkPolyData(dataset) { const arrayToDownload = []; arrayToDownload.push(dataset.points); ['verts', 'lines', 'polys', 'strips'].forEach(cellName => { if (dataset[cellName]) { arrayToDownload.push(dataset[cellName]); } }); return arrayToDownload; }, vtkImageData(dataset) { return []; }, vtkUnstructuredGrid(dataset) { const arrayToDownload = []; arrayToDownload.push(dataset.points); arrayToDownload.push(dataset.cells); arrayToDownload.push(dataset.cellTypes); return arrayToDownload; }, vtkRectilinearGrid(dataset) { const arrayToDownload = []; arrayToDownload.push(dataset.xCoordinates); arrayToDownload.push(dataset.yCoordinates); arrayToDownload.push(dataset.zCoordinates); return arrayToDownload; } }; function processDataSet(publicAPI, model, dataset, fetchArray, resolve, reject, loadData) { const enable = model.enableArray; // Generate array list model.arrays = []; fieldDataLocations.forEach(location => { if (dataset[location]) { dataset[location].arrays.map(i => i.data).forEach(array => { model.arrays.push({ name: array.name, enable, location, array, registration: array.ref.registration || 'addArray' }); }); // Reset data arrays dataset[location].arrays = []; } }); // Fetch geometry arrays const pendingPromises = []; const { progressCallback } = model; const compression = model.fetchGzip ? 'gz' : null; GEOMETRY_ARRAYS[dataset.vtkClass](dataset).forEach(array => { pendingPromises.push(fetchArray(array, { compression, progressCallback })); }); function success() { model.dataset = vtk(dataset); if (!loadData) { model.output[0] = model.dataset; resolve(publicAPI, model.output[0]); } else { publicAPI.loadData().then(() => { model.output[0] = model.dataset; resolve(publicAPI, model.output[0]); }); } } // Wait for all geometry array to be fetched if (pendingPromises.length) { Promise.all(pendingPromises).then(success, err => { reject(err); }); } else { success(); } } // ---------------------------------------------------------------------------- // vtkHttpDataSetReader methods // ---------------------------------------------------------------------------- function vtkHttpDataSetReader(publicAPI, model) { // Set our className model.classHierarchy.push('vtkHttpDataSetReader'); // Empty output by default model.output[0] = vtk({ vtkClass: 'vtkPolyData' }); // Create default dataAccessHelper if not available if (!model.dataAccessHelper) { model.dataAccessHelper = DataAccessHelper.get('http'); } // Internal method to fetch Array function fetchArray(array) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const arrayId = `${array.ref.id}|${array.vtkClass}`; if (!cachedArraysAndPromises[arrayId]) { // Cache the promise while fetching cachedArraysAndPromises[arrayId] = model.dataAccessHelper.fetchArray(publicAPI, model.baseURL, array, options).then(newArray => { // Remove promise and return here if caching is disabled if (!model.maxCacheSize) { delete cachedArraysAndPromises[arrayId]; return newArray; } // Replace the promise with the array in cache once downloaded cachedArraysAndPromises[arrayId] = newArray; cachedArraysMetaData[arrayId] = { lastAccess: new Date() }; // If maxCacheSize is set to -1 the cache is unlimited if (model.maxCacheSize < 0) { return newArray; } // sort cache according to access times (potentially needed for creating space) const cachedArrays = {}; Object.keys(cachedArraysMetaData).forEach(arrId => { cachedArrays[arrId] = { array: cachedArraysAndPromises[arrId], lastAccess: cachedArraysMetaData[arrId].lastAccess }; }); const sortedArrayCache = Object.entries(cachedArrays).sort((a, b) => Math.sign(b[1].lastAccess - a[1].lastAccess)); // Check cache size const cacheSizeLimit = model.maxCacheSize * MiB; let cacheSize = Object.values(cachedArrays).reduce((accSize, entry) => accSize + entry.array.values.byteLength, 0); // Delete cache entries until size is below the limit while (cacheSize > cacheSizeLimit && sortedArrayCache.length > 0) { const [oldId, entry] = sortedArrayCache.pop(); delete cachedArraysAndPromises[oldId]; delete cachedArraysMetaData[oldId]; cacheSize -= entry.array.values.byteLength; } // Edge case: If the new entry is bigger than the cache limit if (!cachedArraysMetaData[arrayId]) { macro.vtkWarningMacro('Cache size is too small for the dataset'); } return newArray; }); } else { // cacheArrays[arrayId] can be a promise or value Promise.resolve(cachedArraysAndPromises[arrayId]).then(cachedArray => { if (array !== cachedArray) { // Update last access for cache retention rules (if caching is enabled) if (model.maxCacheSize) { cachedArraysMetaData[arrayId].lastAccess = new Date(); } // Assign cached array as result Object.assign(array, cachedArray); delete array.ref; } }); } return Promise.resolve(cachedArraysAndPromises[arrayId]); } // Fetch dataset (metadata) publicAPI.updateMetadata = function () { let loadData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (model.compression === 'zip') { return new Promise((resolve, reject) => { DataAccessHelper.get('http').fetchBinary(model.url).then(zipContent => { model.dataAccessHelper = DataAccessHelper.get('zip', { zipContent, callback: zip => { model.baseURL = ''; model.dataAccessHelper.fetchJSON(publicAPI, 'index.json').then(dataset => { publicAPI.parseObject(dataset, { loadData, deepCopy: false }).then(resolve, reject); }, error => { reject(error); }); } }); }, error => { reject(error); }); }); } return new Promise((resolve, reject) => { model.dataAccessHelper.fetchJSON(publicAPI, model.url).then(dataset => { publicAPI.parseObject(dataset, { loadData, deepCopy: false }).then(resolve, reject); }, error => { reject(error); }); }); }; // Set DataSet url publicAPI.setUrl = function (url) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (url.indexOf('index.json') === -1 && !options.fullpath) { model.baseURL = url; model.url = `${url}/index.json`; } else { model.url = url; // Remove the file in the URL const path = url.split('/'); path.pop(); model.baseURL = path.join('/'); } model.compression = options.compression; // Fetch metadata return publicAPI.updateMetadata(!!options.loadData); }; publicAPI.parseObject = (manifest, _ref) => { let { loadData, baseUrl, deepCopy = true } = _ref; if (baseUrl) { model.baseURL = baseUrl; } const dataset = deepCopy ? structuredClone(manifest) : manifest; return new Promise((resolve, reject) => { processDataSet(publicAPI, model, dataset, fetchArray, resolve, reject, loadData); }); }; // Fetch the actual data arrays publicAPI.loadData = () => { const datasetObj = model.dataset; const arrayToFecth = model.arrays.filter(array => array.enable).filter(array => array.array.ref).map(array => array.array); return new Promise((resolve, reject) => { const error = e => { reject(e); }; const processNext = () => { if (arrayToFecth.length) { const { progressCallback } = model; const compression = model.fetchGzip ? 'gz' : null; fetchArray(arrayToFecth.pop(), { compression, progressCallback }).then(processNext, error); } else if (datasetObj) { // Perform array registration on new arrays model.arrays.filter(metaArray => metaArray.registration && !metaArray.array.ref).forEach(metaArray => { const newArray = ARRAY_BUILDERS[metaArray.array.vtkClass].newInstance(metaArray.array); datasetObj[`get${macro.capitalize(metaArray.location)}`]()[metaArray.registration](newArray); delete metaArray.registration; }); datasetObj.modified(); resolve(publicAPI, datasetObj); } }; // Start processing queue processNext(); }); }; publicAPI.requestData = (inData, outData) => { // do nothing loadData will eventually load up the data }; // Toggle arrays to load publicAPI.enableArray = function (location, name) { let enable = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; const activeArray = model.arrays.filter(array => array.name === name && array.location === location); if (activeArray.length === 1) { activeArray[0].enable = enable; } }; // return id's of cached arrays publicAPI.getCachedArrayIds = () => Object.keys(cachedArraysMetaData); // clear global array cache publicAPI.clearCache = () => Object.keys(cachedArraysMetaData).forEach(k => { delete cachedArraysAndPromises[k]; delete cachedArraysMetaData[k]; }); // return Busy state publicAPI.isBusy = () => !!model.requestCount; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { enableArray: true, fetchGzip: false, arrays: [], url: null, baseURL: null, requestCount: 0, arrayCachingEnabled: true, maxCacheSize: 2048 // dataAccessHelper: null, }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Build VTK API macro.obj(publicAPI, model); macro.get(publicAPI, model, ['enableArray', 'fetchGzip', 'url', 'baseURL', 'dataAccessHelper', 'maxCacheSize']); macro.set(publicAPI, model, ['dataAccessHelper', 'progressCallback', 'maxCacheSize']); macro.getArray(publicAPI, model, ['arrays']); macro.algo(publicAPI, model, 0, 1); macro.event(publicAPI, model, 'busy'); // Object methods vtkHttpDataSetReader(publicAPI, model); // Make sure we can destructuring progressCallback from model if (model.progressCallback === undefined) { model.progressCallback = null; } } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkHttpDataSetReader'); // ---------------------------------------------------------------------------- var vtkHttpDataSetReader$1 = { newInstance, extend }; export { vtkHttpDataSetReader$1 as default, extend, newInstance };