diff --git a/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts index 0abff2b3c1a..03744c26d22 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts @@ -3,10 +3,8 @@ import { DisplaySetService, classes } from '@ohif/core'; const ImageSet = classes.ImageSet; const findInstance = (measurement, displaySetService: DisplaySetService) => { - const { - displaySetInstanceUID, - ReferencedSOPInstanceUID: sopUid, - } = measurement; + const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = + measurement; const referencedDisplaySet = displaySetService.getDisplaySetByUID( displaySetInstanceUID ); @@ -52,7 +50,7 @@ const findReferencedInstances = ( const createReferencedImageDisplaySet = (displaySetService, displaySet) => { const instances = findReferencedInstances(displaySetService, displaySet); // This will be a member function of the created image set - const updateInstances = function() { + const updateInstances = function () { this.images.splice( 0, this.images.length, @@ -77,6 +75,8 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => { numImageFrames: instances.length, SOPClassHandlerId: `@ohif/extension-default.sopClassHandlerModule.stack`, isReconstructable: false, + // This object is made of multiple instances from other series + isCompositeStack: true, madeInClient: true, updateInstances, }); diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 4ef8f2bdf8e..0223ffbc9c8 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -2,18 +2,10 @@ import PropTypes from 'prop-types'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core'; -import { CornerstoneServices } from '@ohif/extension-cornerstone/types'; import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; -import { - Icon, - Notification, - Tooltip, - useViewportDialog, - useViewportGrid, - ViewportActionBar, -} from '@ohif/ui'; +import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui'; import hydrateStructuredReport from '../utils/hydrateStructuredReport'; import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; @@ -40,7 +32,7 @@ function OHIFCornerstoneSRViewport(props) { displaySetService, cornerstoneViewportService, measurementService, - } = servicesManager.services as CornerstoneServices; + } = servicesManager.services; // SR viewport will always have a single display set if (displaySets.length > 1) { @@ -50,7 +42,6 @@ function OHIFCornerstoneSRViewport(props) { const srDisplaySet = displaySets[0]; const [viewportGrid, viewportGridService] = useViewportGrid(); - const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [measurementSelected, setMeasurementSelected] = useState(0); const [measurementCount, setMeasurementCount] = useState(1); const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState( diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 1071814bcd6..a55d74edde2 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -9,6 +9,7 @@ import { StackViewport, utilities as csUtils, } from '@cornerstonejs/core'; +import { MeasurementService } from '@ohif/core'; import { CinePlayer, useCine, @@ -30,6 +31,12 @@ import CornerstoneServices from '../types/CornerstoneServices'; const STACK = 'stack'; +/** + * Caches the jump to measurement operation, so that if display set is shown, + * it can jump to the measurement. + */ +let cacheJumpToMeasurementEvent; + function areEqual(prevProps, nextProps) { if (nextProps.needsRerendering) { return false; @@ -412,6 +419,13 @@ const OHIFCornerstoneViewport = React.memo(props => { lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId], }; + let measurement; + if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) { + measurement = cacheJumpToMeasurementEvent.measurement; + // Delete the position presentation so that viewport navigates direct + presentations.positionPresentation = null; + cacheJumpToMeasurementEvent = null; + } cornerstoneViewportService.setViewportData( viewportIndex, @@ -420,6 +434,10 @@ const OHIFCornerstoneViewport = React.memo(props => { displaySetOptions, presentations ); + + if (measurement) { + cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); + } }; loadViewportData(); @@ -536,22 +554,30 @@ function _subscribeToJumpToMeasurementEvents( displaySet => displaySet.displaySetInstanceUID ); const { unsubscribe } = measurementService.subscribe( - measurementService.EVENTS.JUMP_TO_MEASUREMENT, - ({ measurement }) => { - if (!measurement) return; - - // Jump the the measurement if the viewport contains the displaySetUID (fusion) - if (displaysUIDs.includes(measurement.displaySetInstanceUID)) { - _jumpToMeasurement( - measurement, - elementRef, - viewportIndex, - measurementService, - displaySetService, - viewportGridService, - cornerstoneViewportService + MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, + props => { + cacheJumpToMeasurementEvent = props; + const { viewportIndex: jumpIndex, measurement, isConsumed } = props; + if (!measurement || isConsumed) return; + if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) { + // Decide on which viewport should handle this + cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump( + jumpIndex, + measurement.displaySetInstanceUID, + { referencedImageId: measurement.referencedImageId } ); } + if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex) + return; + _jumpToMeasurement( + measurement, + elementRef, + viewportIndex, + measurementService, + displaySetService, + viewportGridService, + cornerstoneViewportService + ); } ); @@ -568,21 +594,19 @@ function _checkForCachedJumpToMeasurementEvents( viewportGridService, cornerstoneViewportService ) { + if (!cacheJumpToMeasurementEvent) return; + if (cacheJumpToMeasurementEvent.isConsumed) { + cacheJumpToMeasurementEvent = null; + return; + } const displaysUIDs = displaySets.map( displaySet => displaySet.displaySetInstanceUID ); if (!displaysUIDs?.length) return; - const measurementIdToJumpTo = measurementService.getJumpToMeasurement( - viewportIndex - ); - - if (measurementIdToJumpTo && elementRef) { - // Jump to measurement if the measurement exists - const measurement = measurementService.getMeasurement( - measurementIdToJumpTo - ); - + // Jump to measurement if the measurement exists + const { measurement } = cacheJumpToMeasurementEvent; + if (measurement && elementRef) { if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) { _jumpToMeasurement( measurement, @@ -611,6 +635,7 @@ function _jumpToMeasurement( if (!SOPInstanceUID) { console.warn('cannot jump in a non-acquisition plane measurements yet'); + return; } const referencedDisplaySet = displaySetService.getDisplaySetByUID( @@ -623,15 +648,17 @@ function _jumpToMeasurement( viewportGridService.setActiveViewportIndex(viewportIndex); - const enableElement = getEnabledElement(targetElement); + const enabledElement = getEnabledElement(targetElement); const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( viewportIndex ); - if (enableElement) { + if (enabledElement) { // See how the jumpToSlice() of Cornerstone3D deals with imageIdx param. - const viewport = enableElement.viewport as IStackViewport | IVolumeViewport; + const viewport = enabledElement.viewport as + | IStackViewport + | IVolumeViewport; let imageIdIndex = 0; let viewportCameraDirectionMatch = true; @@ -680,7 +707,8 @@ function _jumpToMeasurement( cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); // Jump to measurement consumed, remove. - measurementService.removeJumpToMeasurement(viewportIndex); + cacheJumpToMeasurementEvent?.consume?.(); + cacheJumpToMeasurementEvent = null; } } diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index c57d3b0a961..beb45115d20 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -10,6 +10,7 @@ import { utilities as cstUtils, ReferenceLinesTool, } from '@cornerstonejs/tools'; +import { Types as OhifTypes } from '@ohif/core'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import callInputDialog from './utils/callInputDialog'; @@ -19,7 +20,10 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import { CornerstoneServices } from './types'; -function commandsModule({ servicesManager, commandsManager }) { +function commandsModule({ + servicesManager, + commandsManager, +}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { const { viewportGridService, toolGroupService, @@ -484,33 +488,22 @@ function commandsModule({ servicesManager, commandsManager }) { } } }, - firstImage: () => { - // Get current active viewport (return if none active) - const enabledElement = _getActiveViewportEnabledElement(); - if (!enabledElement) { - return; - } - const { viewport } = enabledElement; - - // Check viewport is supported - if ( - viewport! instanceof StackViewport && - viewport! instanceof VolumeViewport - ) { - throw new Error('Unsupported viewport type'); - } - // Set slice to first slice - const options = { imageIndex: 0 }; - cstUtils.jumpToSlice(viewport.element, options); - }, - lastImage: () => { + /** Jumps the active viewport or the specified one to the given slice index */ + jumpToImage: ({ imageIndex, viewport: gridViewport }): void => { // Get current active viewport (return if none active) - const enabledElement = _getActiveViewportEnabledElement(); - if (!enabledElement) { - return; + let viewport; + if (!gridViewport) { + const enabledElement = _getActiveViewportEnabledElement(); + if (!enabledElement) { + return; + } + viewport = enabledElement.viewport; + } else { + viewport = cornerstoneViewportService.getCornerstoneViewport( + gridViewport.id + ); } - const { viewport } = enabledElement; // Get number of slices // -> Copied from cornerstone3D jumpToSlice\_getImageSliceData() @@ -525,8 +518,14 @@ function commandsModule({ servicesManager, commandsManager }) { throw new Error('Unsupported viewport type'); } + const jumpIndex = + imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex; + if (jumpIndex >= numberOfSlices || jumpIndex < 0) { + throw new Error(`Can't jump to ${imageIndex}`); + } + // Set slice to last slice - const options = { imageIndex: numberOfSlices - 1 }; + const options = { imageIndex: jumpIndex }; cstUtils.jumpToSlice(viewport.element, options); }, scroll: ({ direction }) => { @@ -625,8 +624,6 @@ function commandsModule({ servicesManager, commandsManager }) { getNearbyToolData: { commandFn: actions.getNearbyToolData, - storeContexts: [], - options: {}, }, getNearbyAnnotation: { commandFn: actions.getNearbyAnnotation, @@ -636,142 +633,100 @@ function commandsModule({ servicesManager, commandsManager }) { deleteMeasurement: { commandFn: actions.deleteMeasurement, - storeContexts: [], - options: {}, }, setMeasurementLabel: { commandFn: actions.setMeasurementLabel, - storeContexts: [], - options: {}, }, updateMeasurement: { commandFn: actions.updateMeasurement, - storeContexts: [], - options: {}, }, setWindowLevel: { commandFn: actions.setWindowLevel, - storeContexts: [], - options: {}, }, toolbarServiceRecordInteraction: { commandFn: actions.toolbarServiceRecordInteraction, - storeContexts: [], - options: {}, }, setToolActive: { commandFn: actions.setToolActive, - storeContexts: [], - options: {}, }, rotateViewportCW: { commandFn: actions.rotateViewport, - storeContexts: [], options: { rotation: 90 }, }, rotateViewportCCW: { commandFn: actions.rotateViewport, - storeContexts: [], options: { rotation: -90 }, }, incrementActiveViewport: { commandFn: actions.incrementActiveViewport, - storeContexts: [], }, decrementActiveViewport: { commandFn: actions.decrementActiveViewport, - storeContexts: [], }, flipViewportHorizontal: { commandFn: actions.flipViewportHorizontal, - storeContexts: [], - options: {}, }, flipViewportVertical: { commandFn: actions.flipViewportVertical, - storeContexts: [], - options: {}, }, invertViewport: { commandFn: actions.invertViewport, - storeContexts: [], - options: {}, }, resetViewport: { commandFn: actions.resetViewport, - storeContexts: [], - options: {}, }, scaleUpViewport: { commandFn: actions.scaleViewport, - storeContexts: [], options: { direction: 1 }, }, scaleDownViewport: { commandFn: actions.scaleViewport, - storeContexts: [], options: { direction: -1 }, }, fitViewportToWindow: { commandFn: actions.scaleViewport, - storeContexts: [], options: { direction: 0 }, }, nextImage: { commandFn: actions.scroll, - storeContexts: [], options: { direction: 1 }, }, previousImage: { commandFn: actions.scroll, - storeContexts: [], options: { direction: -1 }, }, firstImage: { - commandFn: actions.firstImage, - storeContexts: [], - options: {}, + commandFn: actions.jumpToImage, + options: { imageIndex: 0 }, }, lastImage: { - commandFn: actions.lastImage, - storeContexts: [], - options: {}, + commandFn: actions.jumpToImage, + options: { imageIndex: -1 }, + }, + jumpToImage: { + commandFn: actions.jumpToImage, }, showDownloadViewportModal: { commandFn: actions.showDownloadViewportModal, - storeContexts: [], - options: {}, }, toggleCine: { commandFn: actions.toggleCine, - storeContexts: [], - options: {}, }, arrowTextCallback: { commandFn: actions.arrowTextCallback, - storeContexts: [], - options: {}, }, setViewportActive: { commandFn: actions.setViewportActive, - storeContexts: [], - options: {}, }, setViewportColormap: { commandFn: actions.setViewportColormap, - storeContexts: [], - options: {}, }, toggleStackImageSync: { commandFn: actions.toggleStackImageSync, - storeContexts: [], - options: {}, }, toggleReferenceLines: { commandFn: actions.toggleReferenceLines, - storeContexts: [], - options: {}, }, }; diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index 447dfd654c2..31ea90cb236 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -16,14 +16,14 @@ const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; class CornerstoneCacheService { static REGISTRATION = { - name: 'cornerstoneCacheService', - altName: 'CornerstoneCacheService', + name: 'cornerstoneCacheService', + altName: 'CornerstoneCacheService', create: ({ servicesManager, }: Types.Extensions.ExtensionParams): CornerstoneCacheService => { return new CornerstoneCacheService(servicesManager); - }, - }; + }, + }; stackImageIds: Map = new Map(); volumeImageIds: Map = new Map(); @@ -143,13 +143,18 @@ class CornerstoneCacheService { this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds); } - const { displaySetInstanceUID, StudyInstanceUID } = displaySet; + const { + displaySetInstanceUID, + StudyInstanceUID, + isCompositeStack, + } = displaySet; const StackViewportData: StackViewportData = { viewportType, data: { StudyInstanceUID, displaySetInstanceUID, + isCompositeStack, imageIds: stackImageIds, }, }; diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index cf47cce78d3..012c69e6afd 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -906,6 +906,34 @@ class CornerstoneViewportService extends PubSubService return images[0].FrameOfReferenceUID; } } + + /** + * Looks through the viewports to see if the specified measurement can be + * displayed in one of the viewports. + * + * @param measurement + * The measurement that is desired to view. + * @param activeViewportIndex - the index that was active at the time the jump + * was initiated. + * @return the viewportIndex to display the given measurement + */ + public getViewportIndexToJump( + activeViewportIndex: number, + displaySetInstanceUID: string, + cameraProps: unknown + ): number { + const viewportInfo = this.viewportsInfo.get(activeViewportIndex); + const { referencedImageId } = cameraProps; + if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) { + return activeViewportIndex; + } + + return ( + [...this.viewportsById.values()].find(viewportInfo => + viewportInfo.contains(displaySetInstanceUID, referencedImageId) + )?.viewportIndex ?? -1 + ); + } } export default CornerstoneViewportService; diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index c56af0836cf..a99fa56af9e 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -88,6 +88,20 @@ export type DisplaySet = { const STACK = 'stack'; const DEFAULT_TOOLGROUP_ID = 'default'; +// Return true if the data contains the given display set UID OR the imageId +// if it is a composite object. +const dataContains = ( + data, + displaySetUID: string, + imageId?: string +): boolean => { + if (data.displaySetInstanceUID === displaySetUID) return true; + if (imageId && data.isCompositeStack && data.imageIds) { + return !!data.imageIds.find(dataId => dataId === imageId); + } + return false; +}; + class ViewportInfo { private viewportId = ''; private viewportIndex: number; @@ -104,6 +118,21 @@ class ViewportInfo { this.setPublicDisplaySetOptions([{}]); } + /** + * Return true if the viewport contains the given display set UID, + * OR if it is a composite stack and contains the given imageId + */ + public contains(displaySetUID: string, imageId: string): boolean { + if (!this.viewportData?.data) return false; + + if (this.viewportData.data.length) { + return !!this.viewportData.find(data => + dataContains(data, displaySetUID, imageId) + ); + } + return dataContains(this.viewportData.data, displaySetUID, imageId); + } + public destroy = (): void => { this.element = null; this.viewportData = null; diff --git a/extensions/cornerstone/src/types/CornerstoneCacheService.ts b/extensions/cornerstone/src/types/CornerstoneCacheService.ts index 2a877e9c00b..ba79745a0d3 100644 --- a/extensions/cornerstone/src/types/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/types/CornerstoneCacheService.ts @@ -1,12 +1,12 @@ -import { - Enums, - Types, -} from '@cornerstonejs/core'; - +import { Enums, Types } from '@cornerstonejs/core'; type StackData = { StudyInstanceUID: string; displaySetInstanceUID: string; + // A composite stack is one created from other display sets - kind of like + // madeInClient, but specific to indicating that the imageIds can come from + // different series or even studies. + isCompositeStack?: boolean; imageIds: string[]; frameRate?: number; isClip?: boolean; @@ -20,19 +20,14 @@ type VolumeData = { imageIds?: string[]; }; - type StackViewportData = { +type StackViewportData = { viewportType: Enums.ViewportType; data: StackData; }; - type VolumeViewportData = { +type VolumeViewportData = { viewportType: Enums.ViewportType; data: VolumeData[]; }; -export type { - StackViewportData, - VolumeViewportData, - StackData, - VolumeData, -}; +export type { StackViewportData, VolumeViewportData, StackData, VolumeData }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts index 8dad5a1c124..44cf64ac067 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts @@ -188,6 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + if (length === null || length === undefined) return displayText; const roundedLength = utils.roundNumber(length, 2); displayText.push( `${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})` diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 172f00921c4..238cd37ffea 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -1,4 +1,4 @@ -import { ServicesManager, utils } from '@ohif/core'; +import { ServicesManager, utils, Types } from '@ohif/core'; import { ContextMenuController, @@ -244,6 +244,12 @@ const commandsModule = ({ } } // Do this after successfully applying the update + // Note, don't store the active display set - it is only needed while + // changing display sets. This causes jump to measurement to fail on + // multi-study display. + delete displaySetSelectorMap[ + `${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0` + ]; stateSyncService.store(stateSyncReduce); // This is a default action applied actions.toggleHpTools(hangingProtocolService.getActiveProtocol()); diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index f73707220d3..2a5b0a72d50 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -47,6 +47,15 @@ function TrackedMeasurementsContextProvider( const uid = trackedMeasurements[0].uid; + console.log( + 'jumping to measurement reset viewport', + viewportGrid.activeViewportIndex, + trackedMeasurements[0] + ); + viewportGridService.setDisplaySetsForViewport({ + viewportIndex: viewportGrid.activeViewportIndex, + displaySetInstanceUIDs: [trackedMeasurements[0].displaySetInstanceUID], + }); measurementService.jumpToMeasurement( viewportGrid.activeViewportIndex, uid diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index b1d3e5af7ee..b3c0b3138f3 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -67,7 +67,10 @@ const EVENTS = { RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added', MEASUREMENT_REMOVED: 'event::measurement_removed', MEASUREMENTS_CLEARED: 'event::measurements_cleared', - JUMP_TO_MEASUREMENT: 'event:jump_to_measurement', + // Give the viewport a chance to jump to the measurement + JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport', + // Give the layout a chance to jump to the measurement + JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout', }; const VALUE_TYPES = { @@ -104,15 +107,16 @@ class MeasurementService extends PubSubService { }, }; + public static readonly EVENTS = EVENTS; public static VALUE_TYPES = VALUE_TYPES; public readonly VALUE_TYPES = VALUE_TYPES; + private measurements = new Map(); + constructor() { super(EVENTS); this.sources = {}; this.mappings = {}; - this.measurements = {}; - this._jumpToMeasurementCache = {}; } /** @@ -120,7 +124,7 @@ class MeasurementService extends PubSubService { * This method should be used to add custom tool schema to the measurement service. * @param {Array} schema schema for validation */ - addMeasurementSchemaKeys(schema) { + public addMeasurementSchemaKeys(schema): void { if (!Array.isArray(schema)) { schema = [schema]; } @@ -160,11 +164,7 @@ class MeasurementService extends PubSubService { * @return {Measurement[]} Array of measurements */ getMeasurements() { - const measurements = this._arrayOfObjects(this.measurements); - return ( - measurements && - measurements.map(m => this.measurements[Object.keys(m)[0]]) - ); + return [...this.measurements.values()]; } /** @@ -173,18 +173,14 @@ class MeasurementService extends PubSubService { * @param {string} uid measurement uid * @return {Measurement} Measurement instance */ - getMeasurement(measurementUID) { - let measurement = null; - const measurements = this.measurements[measurementUID]; - - if (measurements && Object.keys(measurements).length > 0) { - measurement = this.measurements[measurementUID]; - } - - return measurement; + public getMeasurement(measurementUID: string) { + return this.measurements.get(measurementUID); } - setMeasurementSelected(measurementUID, selected) { + public setMeasurementSelected( + measurementUID: string, + selected: boolean + ): void { const measurement = this.getMeasurement(measurementUID); if (!measurement) { return; @@ -373,8 +369,8 @@ class MeasurementService extends PubSubService { } } - update(measurementUID, measurement, notYetUpdatedAtSource = false) { - if (!this.measurements[measurementUID]) { + update(measurementUID: string, measurement, notYetUpdatedAtSource = false) { + if (!this.measurements.has(measurementUID)) { return; } @@ -388,7 +384,7 @@ class MeasurementService extends PubSubService { updatedMeasurement ); - this.measurements[measurementUID] = updatedMeasurement; + this.measurements.set(measurementUID, updatedMeasurement); this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { source: measurement.source, @@ -470,15 +466,15 @@ class MeasurementService extends PubSubService { uid: internalUID, }; - if (this.measurements[internalUID]) { - this.measurements[internalUID] = newMeasurement; + if (this.measurements.get(internalUID)) { + this.measurements.set(internalUID, newMeasurement); this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { source, measurement: newMeasurement, }); } else { log.info('Measurement added', newMeasurement); - this.measurements[internalUID] = newMeasurement; + this.measurements.set(internalUID, newMeasurement); this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, { source, measurement: newMeasurement, @@ -557,7 +553,7 @@ class MeasurementService extends PubSubService { ); } - const oldMeasurement = this.measurements[internalUID]; + const oldMeasurement = this.measurements.get(internalUID); const newMeasurement = { ...oldMeasurement, @@ -569,7 +565,7 @@ class MeasurementService extends PubSubService { if (oldMeasurement) { // TODO: Ultimately, each annotation should have a selected flag right from the soure. // For now, it is just added in OHIF here and in setMeasurementSelected. - this.measurements[internalUID] = newMeasurement; + this.measurements.set(internalUID, newMeasurement); if (isUpdate) { this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { source, @@ -585,7 +581,7 @@ class MeasurementService extends PubSubService { } } else { log.info('Measurement started.', newMeasurement); - this.measurements[internalUID] = newMeasurement; + this.measurements.set(internalUID, newMeasurement); } return newMeasurement.uid; @@ -598,12 +594,12 @@ class MeasurementService extends PubSubService { * @param {MeasurementSource} source The measurement source instance */ remove(measurementUID, source, eventDetails) { - if (!measurementUID || !this.measurements[measurementUID]) { + if (!measurementUID || !this.measurements.has(measurementUID)) { log.warn(`No uid provided, or unable to find measurement by uid.`); return; } - delete this.measurements[measurementUID]; + this.measurements.delete(measurementUID); this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, { source, measurement: measurementUID, @@ -613,9 +609,8 @@ class MeasurementService extends PubSubService { clearMeasurements() { // Make a copy of the measurements - const measurements = { ...this.measurements }; - this.measurements = {}; - this._jumpToMeasurementCache = {}; + const measurements = [...this.measurements.values()]; + this.measurements.clear(); this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } @@ -628,27 +623,39 @@ class MeasurementService extends PubSubService { this.clearMeasurements(); } - jumpToMeasurement(viewportIndex, measurementUID) { - const measurement = this.measurements[measurementUID]; + /** + * This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT + * and JUMP_TO_MEASUREMENT_LAYOUT. There are two events which are + * fired because there are two different items which might want to handle + * the event. First, there might already be a viewport which can handle + * the event. If so, then the layout doesn't need to necessarily change. + * This is communicated by the isConsumed value on the event itself. + * Otherwise, the layout itself may need to be navigated to in order + * to provide a viewport which can show the given measurement. + * + * When a viewport decides to apply the event, it should call the consume() + * method on the event, so that other listeners know they do not need to + * navigate. This does NOT affect whether the layout event is fired, and + * merely causes it to fire the event with the isConsumed set to true. + */ + + public jumpToMeasurement( + viewportIndex: number, + measurementUID: string + ): void { + const measurement = this.measurements.get(measurementUID); if (!measurement) { log.warn(`No measurement uid, or unable to find by uid.`); return; } - this._addJumpToMeasurement(viewportIndex, measurementUID); - - this._broadcastEvent(this.EVENTS.JUMP_TO_MEASUREMENT, { + const consumableEvent = this.createConsumableEvent({ viewportIndex, measurement, }); - } - getJumpToMeasurement(viewportIndex) { - return this._jumpToMeasurementCache[viewportIndex]; - } - - removeJumpToMeasurement(viewportIndex) { - delete this._jumpToMeasurementCache[viewportIndex]; + this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent); + this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent); } _getSourceUID(name, version) { @@ -663,10 +670,6 @@ class MeasurementService extends PubSubService { return sourceUID; } - _addJumpToMeasurement(viewportIndex, measurementUID) { - this._jumpToMeasurementCache[viewportIndex] = measurementUID; - } - _getMappingByMeasurementSource(measurement, annotationType) { if (this._isValidSource(measurement.source)) { return this.mappings[measurement.source.uid].find( diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index 239db4f88ba..a5fca6e6a97 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -1,12 +1,13 @@ import { PubSubService } from '../_shared/pubSubServiceInterface'; - -const EVENTS = { - ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged', - LAYOUT_CHANGED: 'event::layoutChanged', - GRID_STATE_CHANGED: 'event::gridStateChanged', -}; +import { getPresentationIds, PresentationIds } from './getPresentationIds'; class ViewportGridService extends PubSubService { + public static readonly EVENTS = { + ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged', + LAYOUT_CHANGED: 'event::layoutChanged', + GRID_STATE_CHANGED: 'event::gridStateChanged', + }; + public static REGISTRATION = { name: 'viewportGridService', altName: 'ViewportGridService', @@ -14,12 +15,13 @@ class ViewportGridService extends PubSubService { return new ViewportGridService(); }, }; - public static EVENTS = EVENTS; + + public static getPresentationIds = getPresentationIds; serviceImplementation = {}; constructor() { - super(EVENTS); + super(ViewportGridService.EVENTS); this.serviceImplementation = {}; } @@ -37,10 +39,12 @@ class ViewportGridService extends PubSubService { this.serviceImplementation._getState = getStateImplementation; } if (setActiveViewportIndexImplementation) { - this.serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation; + this.serviceImplementation._setActiveViewportIndex = + setActiveViewportIndexImplementation; } if (setDisplaySetsForViewportsImplementation) { - this.serviceImplementation._setDisplaySetsForViewports = setDisplaySetsForViewportsImplementation; + this.serviceImplementation._setDisplaySetsForViewports = + setDisplaySetsForViewportsImplementation; } if (setLayoutImplementation) { this.serviceImplementation._setLayout = setLayoutImplementation; @@ -55,7 +59,8 @@ class ViewportGridService extends PubSubService { this.serviceImplementation._set = setImplementation; } if (getNumViewportPanesImplementation) { - this.serviceImplementation._getNumViewportPanes = getNumViewportPanesImplementation; + this.serviceImplementation._getNumViewportPanes = + getNumViewportPanesImplementation; } } @@ -75,11 +80,29 @@ class ViewportGridService extends PubSubService { public setDisplaySetsForViewport(props) { // Just update a single viewport, but use the multi-viewport update for it. - this.serviceImplementation._setDisplaySetsForViewports([props]); + this.setDisplaySetsForViewports([props]); } public setDisplaySetsForViewports(props) { this.serviceImplementation._setDisplaySetsForViewports(props); + const state = this.getState(); + const viewports = []; + + for (const viewport of props) { + const updatedViewport = state.viewports[viewport.viewportIndex]; + if (updatedViewport) { + viewports.push(updatedViewport); + } else { + console.warn( + "ViewportGridService::Didn't find updated viewport", + viewport + ); + } + } + this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, { + state, + viewports, + }); } /** @@ -150,3 +173,5 @@ class ViewportGridService extends PubSubService { } export default ViewportGridService; + +export type { PresentationIds }; diff --git a/platform/ui/src/contextProviders/getPresentationIds.ts b/platform/core/src/services/ViewportGridService/getPresentationIds.ts similarity index 99% rename from platform/ui/src/contextProviders/getPresentationIds.ts rename to platform/core/src/services/ViewportGridService/getPresentationIds.ts index b34b6badca4..4299175115a 100644 --- a/platform/ui/src/contextProviders/getPresentationIds.ts +++ b/platform/core/src/services/ViewportGridService/getPresentationIds.ts @@ -118,3 +118,4 @@ const getPresentationIds = (viewport, viewports): PresentationIds => { }; export default getPresentationIds; +export { getPresentationIds }; diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.js b/platform/core/src/services/_shared/pubSubServiceInterface.js index 20b5f27d785..7d4460fda27 100644 --- a/platform/core/src/services/_shared/pubSubServiceInterface.js +++ b/platform/core/src/services/_shared/pubSubServiceInterface.js @@ -103,4 +103,20 @@ export class PubSubService { this.unsubscriptions.forEach(unsub => unsub()); this.unsubscriptions = []; } + + /** + * Creates an event that records whether or not someone + * has consumed it. Call eventData.consume() to consume the event. + * Check eventData.isConsumed to see if it is consumed or not. + * @param props - to include in the event + */ + protected createConsumableEvent(props) { + return { + ...props, + isConsumed: false, + consume: function Consume() { + this.isConsumed = true; + }, + } + } } diff --git a/platform/core/src/utils/absoluteUrl.test.js b/platform/core/src/utils/absoluteUrl.test.js index 7e69f69b213..fe923064d97 100644 --- a/platform/core/src/utils/absoluteUrl.test.js +++ b/platform/core/src/utils/absoluteUrl.test.js @@ -24,27 +24,22 @@ describe('absoluteUrl', () => { }); test('should return the original path when there path in the window.origin after the domain and port', () => { - global.window = Object.create(window); + delete global.window.location; const url = 'http://dummy.com'; - Object.defineProperty(window, 'location', { - value: { - origin: url, - }, - writable: true, - }); + global.window.location = { + origin: url, + }; const absoluteUrlOutput = absoluteUrl('path_1/path_2/path_3'); expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3'); }); test('should be able to return the absolute path even when the path contains duplicates', () => { - global.window = Object.create(window); + global.window ||= Object.create(window); const url = 'http://dummy.com'; - Object.defineProperty(window, 'location', { - value: { - origin: url, - }, - writable: true, - }); + delete global.window.location; + global.window.location = { + origin: url, + }; const absoluteUrlOutput = absoluteUrl('path_1/path_1/path_1'); expect(absoluteUrlOutput).toEqual('/path_1/path_1/path_1'); }); diff --git a/platform/docs/docs/platform/services/data/MeasurementService.md b/platform/docs/docs/platform/services/data/MeasurementService.md index 74605bce1c5..21bd4589f94 100644 --- a/platform/docs/docs/platform/services/data/MeasurementService.md +++ b/platform/docs/docs/platform/services/data/MeasurementService.md @@ -29,7 +29,8 @@ There are seven events that get publish in `MeasurementService`: | RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) | | MEASUREMENT_REMOVED | Fires when a measurement is removed | | MEASUREMENTS_CLEARED | Fires when all measurements are deleted | -| JUMP_TO_MEASUREMENT | Fires when a measurement is requested to be jump to | +| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. | +| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. | ## API diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx index 85fbb45db4a..1ac89e51917 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx @@ -7,8 +7,8 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import isEqual from 'lodash.isequal'; +import { ViewportGridService } from '@ohif/core'; import viewportLabels from '../utils/viewportLabels'; -import getPresentationIds from './getPresentationIds'; const DEFAULT_STATE = { activeViewportIndex: 0, @@ -173,7 +173,7 @@ export function ViewportGridProvider({ children, service }) { displaySetOptions, viewportLabel: viewportLabels[viewportIndex], }; - viewportOptions.presentationIds = getPresentationIds( + viewportOptions.presentationIds = ViewportGridService.getPresentationIds( newViewport, viewports ); @@ -265,7 +265,7 @@ export function ViewportGridProvider({ children, service }) { state.viewports ); if (!viewport.viewportOptions.presentationIds) { - viewport.viewportOptions.presentationIds = getPresentationIds( + viewport.viewportOptions.presentationIds = ViewportGridService.getPresentationIds( viewport, viewports ); @@ -403,12 +403,16 @@ export function ViewportGridProvider({ children, service }) { getNumViewportPanes, ]); + // run many of the calls through the service itself since we want to publish events const api = { getState, - setActiveViewportIndex: index => service.setActiveViewportIndex(index), // run it through the service itself since we want to publish events - setDisplaySetsForViewports, - setLayout: layout => service.setLayout(layout), // run it through the service itself since we want to publish events - reset, + setActiveViewportIndex: index => service.setActiveViewportIndex(index), + setDisplaySetsForViewport: props => + service.setDisplaySetsForViewports([props]), + setDisplaySetsForViewports: props => + service.setDisplaySetsForViewports(props), + setLayout: layout => service.setLayout(layout), + reset: () => service.reset(), set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events getNumViewportPanes, }; @@ -422,9 +426,7 @@ export function ViewportGridProvider({ children, service }) { ViewportGridProvider.propTypes = { children: PropTypes.any, - service: PropTypes.shape({ - setServiceImplementation: PropTypes.func, - }).isRequired, + service: PropTypes.instanceOf(ViewportGridService).isRequired, }; export const useViewportGrid = () => useContext(ViewportGridContext); diff --git a/platform/ui/src/types/index.ts b/platform/ui/src/types/index.ts index 5d0729fbe4b..284eea27838 100644 --- a/platform/ui/src/types/index.ts +++ b/platform/ui/src/types/index.ts @@ -18,4 +18,4 @@ const StringNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); */ const StringArray = PropTypes.oneOfType([PropTypes.string, PropTypes.array]); -export { StringNumber, StringArray, ThumbnailType, PresentationIds }; +export { StringNumber, StringArray, ThumbnailType }; diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index ca2cc71cc50..dea53511c9c 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { ServicesManager, Types } from '@ohif/core'; +import { ServicesManager, Types, MeasurementService } from '@ohif/core'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { utils } from '@ohif/core'; import EmptyViewport from './EmptyViewport'; @@ -23,13 +23,6 @@ const ORIENTATION_MAP = { }, }; -const compareViewportOptions = (opts1, opts2) => { - if ((opts1.viewportType || 'stack') != opts2.viewportType) { - return false; - } - return true; -}; - function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; const [viewportGrid, viewportGridService] = useViewportGrid(); @@ -160,77 +153,48 @@ function ViewerViewportGrid(props) { useEffect(() => { const { unsubscribe } = measurementService.subscribe( - measurementService.EVENTS.JUMP_TO_MEASUREMENT, - ({ viewportIndex, measurement }) => { - const { - displaySetInstanceUID: referencedDisplaySetInstanceUID, - metadata: { viewPlaneNormal }, - } = measurement; - - // if we already have the displaySet in one of the viewports - // Todo: handle fusion display sets? - for (const viewport of viewports) { - const isMatch = viewport.displaySetInstanceUIDs.includes( - referencedDisplaySetInstanceUID - ); - if (isMatch) { - return; - } - } - - const displaySet = displaySetService.getDisplaySetByUID( - referencedDisplaySetInstanceUID - ); - - let imageIndex; - // jump straight to the initial image index if we can - if (displaySet.images && measurement.SOPInstanceUID) { - imageIndex = displaySet.images.findIndex( - image => image.SOPInstanceUID === measurement.SOPInstanceUID - ); - } + MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, + ({ viewportIndex, measurement, isConsumed }) => { + if (isConsumed) return; + // This occurs when no viewport has elected to consume the event + // so we need to change layouts into a layout which can consume + // the event. + const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = + measurement; const updatedViewports = _getUpdatedViewports( viewportIndex, referencedDisplaySetInstanceUID ); + // Arbitrarily assign the viewport to element 0 + const viewport = updatedViewports?.[0]; - if (!updatedViewports || !updatedViewports.length) { + if (!viewport) { + console.warn( + 'ViewportGrid::Unable to navigate to viewport containing', + referencedDisplaySetInstanceUID + ); return; } - updatedViewports.forEach(vp => { - vp.viewportOptions ||= {}; - const { orientation, viewportType } = vp.viewportOptions; - let initialImageOptions; - - // For initial imageIndex to hang be careful for the volume viewport - if (viewportType === 'stack' || !viewportType) { - initialImageOptions = { - index: imageIndex, - }; - } else if (viewportType === 'volume') { - // For the volume viewports, be careful to not jump in the viewports - // that are not in the same orientation. - // Todo: this doesn't work for viewports that have custom orientation - // vectors specified - if ( - orientation && - viewPlaneNormal && - isEqualWithin( - ORIENTATION_MAP[orientation]?.viewPlaneNormal, - viewPlaneNormal - ) - ) { - initialImageOptions = { - index: imageIndex, + viewport.viewportOptions ||= {}; + viewport.viewportOptions.orientation = 'acquisition'; + + const displaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + // jump straight to the initial image index if we can + if (displaySet.images && measurement.SOPInstanceUID) { + for (let index = 0; index < displaySet.images.length; index++) { + const image = displaySet.images[index]; + if (image.SOPInstanceUID === measurement.SOPInstanceUID) { + viewport.viewportOptions.initialImageOptions = { + index, }; + break; } } - - vp.viewportOptions.initialImageOptions = initialImageOptions; - }); - + } viewportGridService.setDisplaySetsForViewports(updatedViewports); } ); diff --git a/yarn.lock b/yarn.lock index d8eda8ae344..6b51fc00eae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1272,7 +1272,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.11" -"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.21.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -1387,16 +1387,31 @@ resolved "https://registry.npmjs.org/@cornerstonejs/calculate-suv/-/calculate-suv-1.0.3.tgz#6d99a72032c0f90cebf44dc6f0b12a5f1102e884" integrity sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w== +"@cornerstonejs/codec-charls@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-charls/-/codec-charls-0.1.1.tgz#e55d4aa908732d0cc902888b7f3856c5a996df7f" + integrity sha512-Y250DGVzmownJ7WgpHxNqWvfTnv4/malaKm/tWm0xE1FxhQE8iErMWFpKxpNDk3MdfXO4/98piVsUwmJMiWoDQ== + "@cornerstonejs/codec-charls@^1.2.3": version "1.2.3" resolved "https://registry.npmjs.org/@cornerstonejs/codec-charls/-/codec-charls-1.2.3.tgz#6952c420486822ac8404409ae0ed5a559aff6e25" integrity sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA== +"@cornerstonejs/codec-libjpeg-turbo-8bit@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-0.0.7.tgz#2ea9b575eed19e6e7e3701b7a50a4ae0ffbef0c4" + integrity sha512-qgm6BuVAy5mNP8SJ+A6+VbmPnqgj8jPvJrw4HbUoAzndmf9/VHjTYwawn3kmZWya5ErFAsXQ6c0U0noB1LKAiA== + "@cornerstonejs/codec-libjpeg-turbo-8bit@^1.2.2": version "1.2.2" resolved "https://registry.npmjs.org/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144" integrity sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg== +"@cornerstonejs/codec-openjpeg@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b" + integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg== + "@cornerstonejs/codec-openjpeg@^1.2.2": version "1.2.2" resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.2.tgz#f0b524235b5551426b46db197a37b06f8ac805d7" @@ -3408,6 +3423,35 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@mapbox/jsonlint-lines-primitives@~2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" + integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== + +"@mapbox/mapbox-gl-style-spec@^13.23.1": + version "13.28.0" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz#2ec226320a0f77856046e000df9b419303a56458" + integrity sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg== + dependencies: + "@mapbox/jsonlint-lines-primitives" "~2.0.2" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/unitbezier" "^0.0.0" + csscolorparser "~1.0.2" + json-stringify-pretty-compact "^2.0.0" + minimist "^1.2.6" + rw "^1.3.3" + sort-object "^0.3.2" + +"@mapbox/point-geometry@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== + +"@mapbox/unitbezier@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e" + integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== + "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -3669,6 +3713,11 @@ resolved "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.24.0.tgz#b6e83333c437ac106386e10a776dc823c1a90f33" integrity sha512-kfYxX0rHP5N2Da6HyfjRCVaeNahAO9XV5WD4SKWKKjdKVkV/Z5/XjVgSKlTBLSYxnWDzYJJ4UHZV43Mw+facMA== +"@petamoriken/float16@^3.4.7": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.8.0.tgz#3a48b7938e1a62188a61ec02d5b12630f671401f" + integrity sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w== + "@philpl/buble@^0.19.7": version "0.19.7" resolved "https://registry.npmjs.org/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893" @@ -7614,6 +7663,13 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.19: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +colormap@^2.3: + version "2.3.2" + resolved "https://registry.yarnpkg.com/colormap/-/colormap-2.3.2.tgz#4422c1178ce563806e265b96782737be85815abf" + integrity sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA== + dependencies: + lerp "^1.0.3" + colors@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -7697,6 +7753,11 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +complex.js@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" + integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -8332,6 +8393,11 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +csscolorparser@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" + integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== + cssdb@^7.1.0: version "7.5.4" resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.5.4.tgz#e34dafee5184d67634604e345e389ca79ac179ea" @@ -8680,6 +8746,17 @@ dayjs@^1.10.4: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== +dcmjs@^0.27: + version "0.27.0" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.27.0.tgz#2662818c8b20494e366583e6dd3577c20d04d6ff" + integrity sha512-26wtatOLh+0b0aFy9iOg7PdOLG9EHevn9nEOn7Aoo5l7P9aFAMZ3XAa9Q+NULzLE2Q7DcIf2TQvfyVtdzhQzeg== + dependencies: + "@babel/runtime-corejs2" "^7.17.8" + gl-matrix "^3.1.0" + lodash.clonedeep "^4.5.0" + loglevelnext "^3.0.1" + ndarray "^1.0.19" + dcmjs@^0.29.5: version "0.29.6" resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.6.tgz#6ca1543e74bae29657d1f7f2273407e23266c011" @@ -8743,6 +8820,11 @@ decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -9068,16 +9150,43 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dicom-microscopy-viewer@^0.44.0: + version "0.44.0" + resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.44.0.tgz#d4a9e985acb23c5b82a9aedbe379b39198b8fd55" + integrity sha512-7rcm8bXTcOLsXrhBPwWe5gf4Sj1Rz1lRh+ECcaznOEr/uvX6BTAYLqNJNmeAUMoKcadboEeDrmLihCwCteRSJQ== + dependencies: + "@cornerstonejs/codec-charls" "^0.1.1" + "@cornerstonejs/codec-libjpeg-turbo-8bit" "^0.0.7" + "@cornerstonejs/codec-openjpeg" "^0.1.1" + colormap "^2.3" + dcmjs "^0.27" + dicomicc "^0.1" + dicomweb-client "^0.8" + image-type "^4.1" + mathjs "^11.2" + ol "^7.1" + uuid "^9.0" + dicom-parser@^1.8.9: version "1.8.21" resolved "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.21.tgz#916fdc77776367976b8457cad462b5b7cf74eaea" integrity sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w== +dicomicc@^0.1: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dicomicc/-/dicomicc-0.1.0.tgz#c73acc60a8e2d73a20f462c8c7d0e1e0d977c486" + integrity sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ== + dicomweb-client@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.2.tgz#9c2e466264a5d3b56c18edaafd360e89912cac13" integrity sha512-sGjq3TxM7jgbe7cBpqddT1VlfOlwXmE7Q12qCSGDOkEL7fgn+Ak/5oW/LiRP2FzmfYzcbpO32r+hC8i4ITXQLw== +dicomweb-client@^0.8: + version "0.8.4" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74" + integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -9391,6 +9500,11 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +earcut@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -9703,6 +9817,11 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-latex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" + integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -10666,6 +10785,11 @@ file-system-cache@^2.0.0: fs-extra "^11.1.0" ramda "^0.28.0" +file-type@^10.10.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890" + integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -11133,6 +11257,19 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geotiff@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.0.7.tgz#358e578233af70bfb0b4dee62d599ad78fc5cfca" + integrity sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q== + dependencies: + "@petamoriken/float16" "^3.4.7" + lerc "^3.0.0" + pako "^2.0.4" + parse-headers "^2.0.2" + quick-lru "^6.1.1" + web-worker "^1.2.0" + xml-utils "^1.0.2" + get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -12151,7 +12288,7 @@ identity-obj-proxy@3.0.x: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.13: +ieee754@^1.1.12, ieee754@^1.1.13: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -12185,6 +12322,13 @@ image-size@^1.0.1: dependencies: queue "6.0.2" +image-type@^4.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/image-type/-/image-type-4.1.0.tgz#72a88d64ff5021371ed67b9a466442100be57cd1" + integrity sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg== + dependencies: + file-type "^10.10.0" + immer@^9.0.7: version "9.0.21" resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -13127,6 +13271,11 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" +javascript-natural-sort@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" + integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== + jest-canvas-mock@^2.1.0: version "2.5.0" resolved "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.0.tgz#3e60f87f77ddfa273cf8e7e4ea5f86fa827c7117" @@ -13752,6 +13901,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stringify-pretty-compact@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885" + integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -13928,6 +14082,11 @@ left-pad@^1.3.0: resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== +lerc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb" + integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww== + lerna@^3.15.0: version "3.22.1" resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" @@ -13952,6 +14111,11 @@ lerna@^3.15.0: import-local "^2.0.0" npmlog "^4.1.2" +lerp@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/lerp/-/lerp-1.0.3.tgz#a18c8968f917896de15ccfcc28d55a6b731e776e" + integrity sha512-70Rh4rCkJDvwWiTsyZ1HmJGvnyfFah4m6iTux29XmasRiZPDBpT9Cfa4ai73+uLZxnlKruUS62jj2lb11wURiA== + leven@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -14476,6 +14640,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mapbox-to-css-font@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz#a9e31b363ad8ca881cd339ca99f2d2a6b02ea5dd" + integrity sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA== + markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" @@ -14496,6 +14665,21 @@ material-colors@^1.2.1: resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== +mathjs@^11.2: + version "11.8.0" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-11.8.0.tgz#b02e66461ec068fadf1e90c221121704dc14d8f5" + integrity sha512-I7r8HCoqUGyEiHQdeOCF2m2k9N+tcOHO3cZQ3tyJkMMBQMFqMR7dMQEboBMJAiFW2Um3PEItGPwcOc4P6KRqwg== + dependencies: + "@babel/runtime" "^7.21.0" + complex.js "^2.1.1" + decimal.js "^10.4.3" + escape-latex "^1.2.0" + fraction.js "^4.2.0" + javascript-natural-sort "^0.7.1" + seedrandom "^3.0.5" + tiny-emitter "^2.1.0" + typed-function "^4.1.0" + mdast-squeeze-paragraphs@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" @@ -15956,6 +16140,25 @@ oidc-client@1.11.5: crypto-js "^4.0.0" serialize-javascript "^4.0.0" +ol-mapbox-style@^9.2.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz#38a4f7abc8f0a94f378dcdb7cefdcc69ca3f6287" + integrity sha512-YX3u8FBJHsRHaoGxmd724Mp5WPTuV7wLQW6zZhcihMuInsSdCX1EiZfU+8IAL7jG0pbgl5YgC0aWE/MXJcUXxg== + dependencies: + "@mapbox/mapbox-gl-style-spec" "^13.23.1" + mapbox-to-css-font "^2.4.1" + +ol@^7.1: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ol/-/ol-7.3.0.tgz#7ffb5f258dafa4a3e218208aad9054d61f6fe786" + integrity sha512-08vJE4xITKPazQ9qJjeqYjRngnM9s+1eSv219Pdlrjj3LpLqjEH386ncq+76Dw1oGPGR8eLVEePk7FEd9XqqMw== + dependencies: + earcut "^2.2.3" + geotiff "^2.0.7" + ol-mapbox-style "^9.2.0" + pbf "3.2.1" + rbush "^3.0.1" + on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -16301,6 +16504,11 @@ parse-github-repo-url@^1.3.0: resolved "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" integrity sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg== +parse-headers@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9" + integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -16501,6 +16709,14 @@ pause-stream@0.0.11: dependencies: through "~2.3" +pbf@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" + integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + peek-stream@^1.1.0: version "1.1.3" resolved "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" @@ -17588,7 +17804,7 @@ prettier@^1.18.2: prettier@^2.8.0: version "2.8.8" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: @@ -17728,6 +17944,11 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== +protocol-buffers-schema@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== + protocols@^1.4.0: version "1.4.8" resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" @@ -17919,6 +18140,16 @@ quick-lru@^5.1.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +quick-lru@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.1.tgz#f8e5bf9010376c126c80c1a62827a526c0e60adf" + integrity sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q== + +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + raf@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -17958,6 +18189,13 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +rbush@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" + integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== + dependencies: + quickselect "^2.0.0" + rc@1.2.8, rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -19043,6 +19281,13 @@ resolve-pathname@^3.0.0: resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -19219,6 +19464,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rw@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + rxjs@^6.3.3, rxjs@^6.4.0: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -19368,7 +19618,7 @@ section-matter@^1.0.0: extend-shallow "^2.0.1" kind-of "^6.0.0" -seedrandom@3.0.5: +seedrandom@3.0.5, seedrandom@^3.0.5: version "3.0.5" resolved "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== @@ -19813,11 +20063,21 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sort-asc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9" + integrity sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw== + sort-css-media-queries@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== +sort-desc@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.1.1.tgz#198b8c0cdeb095c463341861e3925d4ee359a9ee" + integrity sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw== + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -19832,6 +20092,14 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sort-object@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-0.3.2.tgz#98e0d199ede40e07c61a84403c61d6c3b290f9e2" + integrity sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA== + dependencies: + sort-asc "^0.1.0" + sort-desc "^0.1.1" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -20823,6 +21091,11 @@ timsort@^0.3.0: resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== +tiny-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + tiny-invariant@^1.0.2: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -21084,6 +21357,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typed-function@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.0.tgz#da4bdd8a6d19a89e22732f75e4a410860aaf9712" + integrity sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -21580,7 +21858,7 @@ uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: +uuid@^9.0, uuid@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== @@ -21779,6 +22057,11 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== +web-worker@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" + integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== + webgl-constants@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855" @@ -22499,6 +22782,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-utils@^1.0.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.7.0.tgz#333ce391d8918f872aaf98d2cf90f9ef9b82bd0f" + integrity sha512-bWB489+RQQclC7A9OW8e5BzbT8Tu//jtAOvkYwewFr+Q9T9KDGvfzC1lp0pYPEQPEoPQLDkmxkepSC/2gIAZGw== + xml@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"