UNPKG

@cornerstonejs/core

Version:
686 lines (685 loc) • 27.5 kB
import triggerEvent from '../utilities/triggerEvent.js'; import imageIdToURI from '../utilities/imageIdToURI.js'; import eventTarget from '../eventTarget.js'; import Events from '../enums/Events.js'; import { ImageQualityStatus } from '../enums/index.js'; import fnv1aHash from '../utilities/fnv1aHash.js'; const ONE_GB = 1073741824; class Cache { constructor() { this._imageCache = new Map(); this._volumeCache = new Map(); this._imageIdsToVolumeIdCache = new Map(); this._referencedImageIdToImageIdCache = new Map(); this._geometryCache = new Map(); this._imageCacheSize = 0; this._maxCacheSize = 3 * ONE_GB; this._geometryCacheSize = 0; this.setMaxCacheSize = (newMaxCacheSize) => { if (!newMaxCacheSize || typeof newMaxCacheSize !== 'number') { const errorMessage = `New max cacheSize ${this._maxCacheSize} should be defined and should be a number.`; throw new Error(errorMessage); } this._maxCacheSize = newMaxCacheSize; }; this.isCacheable = (byteLength) => { const bytesAvailable = this.getBytesAvailable(); const purgableImageBytes = Array.from(this._imageCache.values()).reduce((total, image) => { if (!image.sharedCacheKey) { return total + image.sizeInBytes; } return total; }, 0); const availableSpaceWithoutSharedCacheKey = bytesAvailable + purgableImageBytes; return availableSpaceWithoutSharedCacheKey >= byteLength; }; this.getMaxCacheSize = () => this._maxCacheSize; this.getCacheSize = () => this._imageCacheSize; this._decacheImage = (imageId, force = false) => { const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { return; } if (cachedImage.sharedCacheKey && !force) { throw new Error('Cannot decache an image with a shared cache key. You need to manually decache the volume first.'); } const { imageLoadObject } = cachedImage; if (cachedImage.image?.referencedImageId) { this._referencedImageIdToImageIdCache.delete(cachedImage.image.referencedImageId); } if (imageLoadObject?.cancelFn) { imageLoadObject.cancelFn(); } if (imageLoadObject?.decache) { imageLoadObject.decache(); } this._imageCache.delete(imageId); }; this._decacheVolume = (volumeId) => { const cachedVolume = this._volumeCache.get(volumeId); if (!cachedVolume) { return; } const { volumeLoadObject, volume } = cachedVolume; if (!volume) { return; } if (volume.cancelLoading) { volume.cancelLoading(); } if (volume.imageData) { volume.imageData.delete(); } if (volumeLoadObject.cancelFn) { volumeLoadObject.cancelFn(); } if (volume.imageIds) { volume.imageIds.forEach((imageId) => { const cachedImage = this._imageCache.get(imageId); if (cachedImage && cachedImage.sharedCacheKey === volumeId) { cachedImage.sharedCacheKey = undefined; } }); } this._volumeCache.delete(volumeId); }; this.purgeCache = () => { const imageIterator = this._imageCache.keys(); this.purgeVolumeCache(); while (true) { const { value: imageId, done } = imageIterator.next(); if (done) { break; } this.removeImageLoadObject(imageId, { force: true }); triggerEvent(eventTarget, Events.IMAGE_CACHE_IMAGE_REMOVED, { imageId }); } }; this.purgeVolumeCache = () => { const volumeIterator = this._volumeCache.keys(); while (true) { const { value: volumeId, done } = volumeIterator.next(); if (done) { break; } this.removeVolumeLoadObject(volumeId); triggerEvent(eventTarget, Events.VOLUME_CACHE_VOLUME_REMOVED, { volumeId, }); } }; this.getVolumeLoadObject = (volumeId) => { if (volumeId === undefined) { throw new Error('getVolumeLoadObject: volumeId must not be undefined'); } const cachedVolume = this._volumeCache.get(volumeId); if (!cachedVolume) { return; } cachedVolume.timeStamp = Date.now(); return cachedVolume.volumeLoadObject; }; this.putGeometryLoadObject = (geometryId, geometryLoadObject) => { if (geometryId === undefined) { throw new Error('putGeometryLoadObject: geometryId must not be undefined'); } if (geometryLoadObject.promise === undefined) { throw new Error('putGeometryLoadObject: geometryLoadObject.promise must not be undefined'); } if (this._geometryCache.has(geometryId)) { throw new Error('putGeometryLoadObject: geometryId already present in geometryCache'); } if (geometryLoadObject.cancelFn && typeof geometryLoadObject.cancelFn !== 'function') { throw new Error('putGeometryLoadObject: geometryLoadObject.cancel must be a function'); } const cachedGeometry = { loaded: false, geometryId, geometryLoadObject, timeStamp: Date.now(), sizeInBytes: 0, }; this._geometryCache.set(geometryId, cachedGeometry); return geometryLoadObject.promise .then((geometry) => { try { this._putGeometryCommon(geometryId, geometry, cachedGeometry); } catch (error) { console.debug(`Error in _putGeometryCommon for geometry ${geometryId}:`, error); throw error; } }) .catch((error) => { console.debug(`Error caching geometry ${geometryId}:`, error); this._geometryCache.delete(geometryId); throw error; }); }; this.getGeometry = (geometryId) => { if (geometryId === undefined) { throw new Error('getGeometry: geometryId must not be undefined'); } const cachedGeometry = this._geometryCache.get(geometryId); if (!cachedGeometry) { return; } cachedGeometry.timeStamp = Date.now(); return cachedGeometry.geometry; }; this.removeGeometryLoadObject = (geometryId) => { if (geometryId === undefined) { throw new Error('removeGeometryLoadObject: geometryId must not be undefined'); } const cachedGeometry = this._geometryCache.get(geometryId); if (!cachedGeometry) { throw new Error('removeGeometryLoadObject: geometryId was not present in geometryCache'); } this.decrementGeometryCacheSize(cachedGeometry.sizeInBytes); const eventDetails = { geometry: cachedGeometry, geometryId, }; triggerEvent(eventTarget, Events.GEOMETRY_CACHE_GEOMETRY_REMOVED, eventDetails); this._decacheGeometry(geometryId); }; this._decacheGeometry = (geometryId) => { const cachedGeometry = this._geometryCache.get(geometryId); if (!cachedGeometry) { return; } const { geometryLoadObject } = cachedGeometry; if (geometryLoadObject.cancelFn) { geometryLoadObject.cancelFn(); } if (geometryLoadObject.decache) { geometryLoadObject.decache(); } this._geometryCache.delete(geometryId); }; this.incrementGeometryCacheSize = (increment) => { this._geometryCacheSize += increment; }; this.decrementGeometryCacheSize = (decrement) => { this._geometryCacheSize -= decrement; }; this.getImageByReferencedImageId = (referencedImageId) => { const imageId = this._referencedImageIdToImageIdCache.get(referencedImageId); if (imageId) { return this._imageCache.get(imageId)?.image; } return undefined; }; this.getImage = (imageId, minQuality = ImageQualityStatus.FAR_REPLICATE) => { if (imageId === undefined) { throw new Error('getImage: imageId must not be undefined'); } const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { return; } cachedImage.timeStamp = Date.now(); if (cachedImage.image?.imageQualityStatus < minQuality) { return; } return cachedImage.image; }; this.getVolume = (volumeId, allowPartialMatch = false) => { if (volumeId === undefined) { throw new Error('getVolume: volumeId must not be undefined'); } const cachedVolume = this._volumeCache.get(volumeId); if (!cachedVolume) { return allowPartialMatch ? [...this._volumeCache.values()].find((cv) => cv.volumeId.includes(volumeId))?.volume : undefined; } cachedVolume.timeStamp = Date.now(); return cachedVolume.volume; }; this.getVolumes = () => { const cachedVolumes = Array.from(this._volumeCache.values()); return cachedVolumes.map((cachedVolume) => cachedVolume.volume); }; this.filterVolumesByReferenceId = (volumeId) => { const cachedVolumes = this.getVolumes(); return cachedVolumes.filter((volume) => { return volume.referencedVolumeId === volumeId; }); }; this.removeImageLoadObject = (imageId, { force = false } = {}) => { if (imageId === undefined) { throw new Error('removeImageLoadObject: imageId must not be undefined'); } const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { throw new Error('removeImageLoadObject: imageId was not present in imageCache'); } this._decacheImage(imageId, force); this.incrementImageCacheSize(-cachedImage.sizeInBytes); const eventDetails = { image: cachedImage, imageId, }; triggerEvent(eventTarget, Events.IMAGE_CACHE_IMAGE_REMOVED, eventDetails); }; this.removeVolumeLoadObject = (volumeId) => { if (volumeId === undefined) { throw new Error('removeVolumeLoadObject: volumeId must not be undefined'); } const cachedVolume = this._volumeCache.get(volumeId); if (!cachedVolume) { throw new Error('removeVolumeLoadObject: volumeId was not present in volumeCache'); } const eventDetails = { volume: cachedVolume, volumeId, }; triggerEvent(eventTarget, Events.VOLUME_CACHE_VOLUME_REMOVED, eventDetails); this._decacheVolume(volumeId); }; this.incrementImageCacheSize = (increment) => { this._imageCacheSize += increment; }; this.decrementImageCacheSize = (decrement) => { this._imageCacheSize -= decrement; }; this.getGeometryLoadObject = (geometryId) => { if (geometryId === undefined) { throw new Error('getGeometryLoadObject: geometryId must not be undefined'); } const cachedGeometry = this._geometryCache.get(geometryId); if (!cachedGeometry) { return; } cachedGeometry.timeStamp = Date.now(); return cachedGeometry.geometryLoadObject; }; } generateVolumeId(imageIds) { const imageURIs = imageIds.map(imageIdToURI).sort(); let combinedHash = 0x811c9dc5; for (const id of imageURIs) { const idHash = fnv1aHash(id); for (let i = 0; i < idHash.length; i++) { combinedHash ^= idHash.charCodeAt(i); combinedHash += (combinedHash << 1) + (combinedHash << 4) + (combinedHash << 7) + (combinedHash << 8) + (combinedHash << 24); } } return `volume-${(combinedHash >>> 0).toString(36)}`; } getImageIdsForVolumeId(volumeId) { return Array.from(this._imageIdsToVolumeIdCache.entries()) .filter(([_, id]) => id === volumeId) .map(([key]) => key); } getBytesAvailable() { return this.getMaxCacheSize() - this.getCacheSize(); } decacheIfNecessaryUntilBytesAvailable(numBytes, volumeImageIds) { let bytesAvailable = this.getBytesAvailable(); if (bytesAvailable >= numBytes) { return bytesAvailable; } const cachedImages = Array.from(this._imageCache.values()).filter((cachedImage) => !cachedImage.sharedCacheKey); function compare(a, b) { if (a.timeStamp > b.timeStamp) { return 1; } if (a.timeStamp < b.timeStamp) { return -1; } return 0; } cachedImages.sort(compare); const cachedImageIds = cachedImages.map((im) => im.imageId); let imageIdsToPurge = cachedImageIds; if (volumeImageIds) { imageIdsToPurge = cachedImageIds.filter((id) => !volumeImageIds.includes(id)); } for (const imageId of imageIdsToPurge) { this.removeImageLoadObject(imageId); triggerEvent(eventTarget, Events.IMAGE_CACHE_IMAGE_REMOVED, { imageId }); bytesAvailable = this.getBytesAvailable(); if (bytesAvailable >= numBytes) { return bytesAvailable; } } for (const imageId of cachedImageIds) { this.removeImageLoadObject(imageId); triggerEvent(eventTarget, Events.IMAGE_CACHE_IMAGE_REMOVED, { imageId }); bytesAvailable = this.getBytesAvailable(); if (bytesAvailable >= numBytes) { return bytesAvailable; } } } _putImageCommon(imageId, image, cachedImage) { if (!this._imageCache.has(imageId)) { console.warn('The image was purged from the cache before it completed loading.'); return; } if (!image) { console.warn('Image is undefined'); return; } if (image.sizeInBytes === undefined || Number.isNaN(image.sizeInBytes)) { throw new Error('_putImageCommon: image.sizeInBytes must not be undefined'); } if (image.sizeInBytes.toFixed === undefined) { throw new Error('_putImageCommon: image.sizeInBytes is not a number'); } if (!this.isCacheable(image.sizeInBytes)) { throw new Error(Events.CACHE_SIZE_EXCEEDED); } this.decacheIfNecessaryUntilBytesAvailable(image.sizeInBytes); cachedImage.loaded = true; cachedImage.image = image; cachedImage.sizeInBytes = image.sizeInBytes; this.incrementImageCacheSize(cachedImage.sizeInBytes); const eventDetails = { image: cachedImage, }; if (image.referencedImageId) { this._referencedImageIdToImageIdCache.set(image.referencedImageId, imageId); } triggerEvent(eventTarget, Events.IMAGE_CACHE_IMAGE_ADDED, eventDetails); cachedImage.sharedCacheKey = image.sharedCacheKey; } async putImageLoadObject(imageId, imageLoadObject) { if (imageId === undefined) { console.error('putImageLoadObject: imageId must not be undefined'); throw new Error('putImageLoadObject: imageId must not be undefined'); } if (imageLoadObject.promise === undefined) { console.error('putImageLoadObject: imageLoadObject.promise must not be undefined'); throw new Error('putImageLoadObject: imageLoadObject.promise must not be undefined'); } const alreadyCached = this._imageCache.get(imageId); if (alreadyCached?.imageLoadObject) { console.warn(`putImageLoadObject: imageId ${imageId} already in cache`); throw new Error('putImageLoadObject: imageId already in cache'); } if (imageLoadObject.cancelFn && typeof imageLoadObject.cancelFn !== 'function') { console.error('putImageLoadObject: imageLoadObject.cancel must be a function'); throw new Error('putImageLoadObject: imageLoadObject.cancel must be a function'); } const cachedImage = { ...alreadyCached, loaded: false, imageId, sharedCacheKey: undefined, imageLoadObject, timeStamp: Date.now(), sizeInBytes: 0, }; this._imageCache.set(imageId, cachedImage); return imageLoadObject.promise .then((image) => { try { this._putImageCommon(imageId, image, cachedImage); } catch (error) { console.debug(`Error in _putImageCommon for image ${imageId}:`, error); throw error; } }) .catch((error) => { console.debug(`Error caching image ${imageId}:`, error); this._imageCache.delete(imageId); throw error; }); } putImageSync(imageId, image) { if (imageId === undefined) { throw new Error('putImageSync: imageId must not be undefined'); } if (this._imageCache.has(imageId)) { throw new Error('putImageSync: imageId already in cache'); } const cachedImage = { loaded: false, imageId, sharedCacheKey: undefined, imageLoadObject: { promise: Promise.resolve(image), }, timeStamp: Date.now(), sizeInBytes: 0, }; this._imageCache.set(imageId, cachedImage); try { this._putImageCommon(imageId, image, cachedImage); } catch (error) { this._imageCache.delete(imageId); throw error; } } getImageLoadObject(imageId) { if (imageId === undefined) { throw new Error('getImageLoadObject: imageId must not be undefined'); } const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { return; } cachedImage.timeStamp = Date.now(); return cachedImage.imageLoadObject; } isLoaded(imageId) { const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { return false; } return cachedImage.loaded; } getVolumeContainingImageId(imageId) { const volumeIds = Array.from(this._volumeCache.keys()); const imageIdToUse = imageIdToURI(imageId); for (const volumeId of volumeIds) { const cachedVolume = this._volumeCache.get(volumeId); if (!cachedVolume) { return; } const { volume } = cachedVolume; if (!volume.imageIds.length) { return; } const imageIdIndex = volume.getImageURIIndex(imageIdToUse); if (imageIdIndex > -1) { return { volume, imageIdIndex }; } } } getCachedImageBasedOnImageURI(imageId) { const imageURIToUse = imageIdToURI(imageId); const cachedImageIds = Array.from(this._imageCache.keys()); const foundImageId = cachedImageIds.find((imageId) => { return imageIdToURI(imageId) === imageURIToUse; }); if (!foundImageId) { return; } return this._imageCache.get(foundImageId); } _putVolumeCommon(volumeId, volume, cachedVolume) { if (!this._volumeCache.get(volumeId)) { console.warn('The volume was purged from the cache before it completed loading.'); return; } cachedVolume.loaded = true; cachedVolume.volume = volume; volume.imageIds?.forEach((imageId) => { const image = this._imageCache.get(imageId); if (image) { image.sharedCacheKey = volumeId; } }); const eventDetails = { volume: cachedVolume, }; triggerEvent(eventTarget, Events.VOLUME_CACHE_VOLUME_ADDED, eventDetails); } putVolumeSync(volumeId, volume) { if (volumeId === undefined) { throw new Error('putVolumeSync: volumeId must not be undefined'); } if (this._volumeCache.has(volumeId)) { throw new Error('putVolumeSync: volumeId already in cache'); } const cachedVolume = { loaded: false, volumeId, volumeLoadObject: { promise: Promise.resolve(volume), }, timeStamp: Date.now(), sizeInBytes: 0, }; this._volumeCache.set(volumeId, cachedVolume); try { this._putVolumeCommon(volumeId, volume, cachedVolume); } catch (error) { this._volumeCache.delete(volumeId); throw error; } } async putVolumeLoadObject(volumeId, volumeLoadObject) { if (volumeId === undefined) { throw new Error('putVolumeLoadObject: volumeId must not be undefined'); } if (volumeLoadObject.promise === undefined) { throw new Error('putVolumeLoadObject: volumeLoadObject.promise must not be undefined'); } if (this._volumeCache.has(volumeId)) { throw new Error(`putVolumeLoadObject: volumeId:${volumeId} already in cache`); } if (volumeLoadObject.cancelFn && typeof volumeLoadObject.cancelFn !== 'function') { throw new Error('putVolumeLoadObject: volumeLoadObject.cancel must be a function'); } const cachedVolume = { loaded: false, volumeId, volumeLoadObject, timeStamp: Date.now(), sizeInBytes: 0, }; this._volumeCache.set(volumeId, cachedVolume); return volumeLoadObject.promise .then((volume) => { try { this._putVolumeCommon(volumeId, volume, cachedVolume); } catch (error) { console.error(`Error in _putVolumeCommon for volume ${volumeId}:`, error); this._volumeCache.delete(volumeId); throw error; } }) .catch((error) => { this._volumeCache.delete(volumeId); throw error; }); } _putGeometryCommon(geometryId, geometry, cachedGeometry) { if (!this._geometryCache.get(geometryId)) { console.warn('The geometry was purged from the cache before it completed loading.'); return; } if (!geometry) { console.warn('Geometry is undefined'); return; } if (geometry.sizeInBytes === undefined || Number.isNaN(geometry.sizeInBytes)) { throw new Error('_putGeometryCommon: geometry.sizeInBytes must not be undefined'); } if (geometry.sizeInBytes.toFixed === undefined) { throw new Error('_putGeometryCommon: geometry.sizeInBytes is not a number'); } if (!this.isCacheable(geometry.sizeInBytes)) { throw new Error(Events.CACHE_SIZE_EXCEEDED); } this.decacheIfNecessaryUntilBytesAvailable(geometry.sizeInBytes); cachedGeometry.loaded = true; cachedGeometry.geometry = geometry; cachedGeometry.sizeInBytes = geometry.sizeInBytes; this.incrementGeometryCacheSize(cachedGeometry.sizeInBytes); const eventDetails = { geometry: cachedGeometry, }; triggerEvent(eventTarget, Events.GEOMETRY_CACHE_GEOMETRY_ADDED, eventDetails); } putGeometrySync(geometryId, geometry) { if (geometryId === undefined) { throw new Error('putGeometrySync: geometryId must not be undefined'); } if (this._geometryCache.has(geometryId)) { throw new Error('putGeometrySync: geometryId already in cache'); } const cachedGeometry = { loaded: false, geometryId, geometryLoadObject: { promise: Promise.resolve(geometry), }, timeStamp: Date.now(), sizeInBytes: 0, }; this._geometryCache.set(geometryId, cachedGeometry); try { this._putGeometryCommon(geometryId, geometry, cachedGeometry); } catch (error) { this._geometryCache.delete(geometryId); throw error; } } setPartialImage(imageId, partialImage, removeImageLoadObject = false) { const cachedImage = this._imageCache.get(imageId); if (!cachedImage) { if (partialImage) { this._imageCache.set(imageId, { image: partialImage, imageId, loaded: false, timeStamp: Date.now(), sizeInBytes: 0, }); } return; } if (cachedImage.loaded) { cachedImage.loaded = false; cachedImage.imageLoadObject = null; this.incrementImageCacheSize(-cachedImage.sizeInBytes); cachedImage.sizeInBytes = 0; cachedImage.image = partialImage || cachedImage.image; } else { if (removeImageLoadObject) { cachedImage.imageLoadObject = null; } cachedImage.image = partialImage || cachedImage.image; } } getImageQuality(imageId) { const image = this._imageCache.get(imageId)?.image; return image ? image.imageQualityStatus || ImageQualityStatus.FULL_RESOLUTION : undefined; } } const cache = new Cache(); export default cache; export { Cache, cache };