@cornerstonejs/core
Version:
Cornerstone3D Core
686 lines (685 loc) • 27.5 kB
JavaScript
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 };