From 925efb6da2b9bbc01f5f0913cdc33e784be87d9e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 11 Apr 2024 14:48:10 -0400 Subject: [PATCH 1/8] feat(segmentGroups): create segments from converted image --- src/store/segmentGroups.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/segmentGroups.ts b/src/store/segmentGroups.ts index acd2c532..312d7c7c 100644 --- a/src/store/segmentGroups.ts +++ b/src/store/segmentGroups.ts @@ -396,7 +396,7 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => { async function serialize(state: StateFile) { const { zip } = state; - // orderByParent is implicity preserved based on + // orderByParent is implicitly preserved based on // the order of serialized entries. const parents = Object.keys(orderByParent.value); From c9d705b4f8d2e260bacbbb0aad06df794c8d76d5 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Apr 2024 18:03:25 -0400 Subject: [PATCH 2/8] feat(load-data): name match DataSources as segment groups If a secondary DataSource has a name like foo.segmentation.bar and the primary DataSource is named foo.baz, the secondary DataSource will be loaded as a segment group. --- src/store/load-data.ts | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/store/load-data.ts b/src/store/load-data.ts index 45b860d5..b265609e 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -159,12 +159,7 @@ function pickBaseDicom(loadableDataSources: Array) { return undefined; } -function getStudyUID(volumeID: string) { - const dicomStore = useDICOMStore(); - const studyKey = dicomStore.volumeStudy[volumeID]; - return dicomStore.studyInfo[studyKey]?.StudyInstanceUID; -} - +// returns image and dicom sources, no config files function pickLoadableDataSources( succeeded: Array> ) { @@ -173,6 +168,38 @@ function pickLoadableDataSources( }); } +// Returns list of dataSources with file names where the name has the extension argument +// and the start of the file name matches the primary file name. +function pickMatchingNames( + primaryDataSource: VolumeResult, + succeeded: Array>, + extension: string = 'segmentation' +) { + const primaryName = getDataSourceName(primaryDataSource.dataSource); + if (!primaryName) return []; + const primaryNamePrefix = primaryName.split('.').slice(0, 1).join(); + return pickLoadableDataSources(succeeded) + .filter((ds) => ds !== primaryDataSource) + .map((importResult) => ({ + importResult, + name: getDataSourceName(importResult.dataSource), + })) + .filter(({ name }) => { + if (!name) return false; + const extensions = name.split('.').slice(1); + const hasExtension = extensions.includes(extension); + const nameMatchesPrimary = name.startsWith(primaryNamePrefix); + return hasExtension && nameMatchesPrimary; + }) + .map(({ importResult }) => importResult); +} + +function getStudyUID(volumeID: string) { + const dicomStore = useDICOMStore(); + const studyKey = dicomStore.volumeStudy[volumeID]; + return dicomStore.studyInfo[studyKey]?.StudyInstanceUID; +} + function pickBaseDataSource( succeeded: Array> ) { @@ -227,6 +254,12 @@ function loadSegmentations( primaryDataSource: VolumeResult, succeeded: Array> ) { + const matchingNames = pickMatchingNames( + primaryDataSource, + succeeded, + 'segmentation' + ).filter(isVolumeResult); // filter out models + const dicomStore = useDICOMStore(); const otherSegVolumesInStudy = pickOtherVolumesInStudy( primaryDataSource.dataID, @@ -238,7 +271,7 @@ function loadSegmentations( }); const segmentGroupStore = useSegmentGroupStore(); - otherSegVolumesInStudy.forEach((ds) => { + [...otherSegVolumesInStudy, ...matchingNames].forEach((ds) => { const loadable = toDataSelection(ds); segmentGroupStore.convertImageToLabelmap( loadable, From f53f5e42ed9522a5419362266d42d3dc51e726cb Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Apr 2024 18:21:34 -0400 Subject: [PATCH 3/8] refactor(load-data): rename function prefixes to filter and find More consistent with Array JS functions. --- src/store/load-data.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/store/load-data.ts b/src/store/load-data.ts index b265609e..96965751 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -122,8 +122,8 @@ function useLoadingNotifications() { }; } -function pickBaseDicom(loadableDataSources: Array) { - // pick dicom dataset as primary selection if available +function findBaseDicom(loadableDataSources: Array) { + // find dicom dataset for primary selection if available const dicoms = loadableDataSources.filter( ({ dataType }) => dataType === 'dicom' ); @@ -160,7 +160,7 @@ function pickBaseDicom(loadableDataSources: Array) { } // returns image and dicom sources, no config files -function pickLoadableDataSources( +function filterLoadableDataSources( succeeded: Array> ) { return succeeded.flatMap((result) => { @@ -170,15 +170,15 @@ function pickLoadableDataSources( // Returns list of dataSources with file names where the name has the extension argument // and the start of the file name matches the primary file name. -function pickMatchingNames( +function filterMatchingNames( primaryDataSource: VolumeResult, succeeded: Array>, - extension: string = 'segmentation' + extension: string ) { const primaryName = getDataSourceName(primaryDataSource.dataSource); if (!primaryName) return []; const primaryNamePrefix = primaryName.split('.').slice(0, 1).join(); - return pickLoadableDataSources(succeeded) + return filterLoadableDataSources(succeeded) .filter((ds) => ds !== primaryDataSource) .map((importResult) => ({ importResult, @@ -200,20 +200,20 @@ function getStudyUID(volumeID: string) { return dicomStore.studyInfo[studyKey]?.StudyInstanceUID; } -function pickBaseDataSource( +function findBaseDataSource( succeeded: Array> ) { - const loadableDataSources = pickLoadableDataSources(succeeded); - const baseDicom = pickBaseDicom(loadableDataSources); + const loadableDataSources = filterLoadableDataSources(succeeded); + const baseDicom = findBaseDicom(loadableDataSources); return baseDicom ?? loadableDataSources[0]; } -function pickOtherVolumesInStudy( +function filterOtherVolumesInStudy( volumeID: string, succeeded: Array> ) { const targetStudyUID = getStudyUID(volumeID); - const dicomDataSources = pickLoadableDataSources(succeeded).filter( + const dicomDataSources = filterLoadableDataSources(succeeded).filter( ({ dataType }) => dataType === 'dicom' ); return dicomDataSources.filter((ds) => { @@ -228,7 +228,7 @@ function loadLayers( succeeded: Array> ) { if (primaryDataSource.dataType !== 'dicom') return; - const otherVolumesInStudy = pickOtherVolumesInStudy( + const otherVolumesInStudy = filterOtherVolumesInStudy( primaryDataSource.dataID, succeeded ); @@ -249,19 +249,21 @@ function loadLayers( layersStore.addLayer(primarySelection, layerSelection); } -// Loads DICOM SEG modalities as Segment Groups if found +// Loads other DataSources Segment Groups: +// - DICOM SEG modalities with matching StudyUIDs. +// - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz function loadSegmentations( primaryDataSource: VolumeResult, succeeded: Array> ) { - const matchingNames = pickMatchingNames( + const matchingNames = filterMatchingNames( primaryDataSource, succeeded, 'segmentation' ).filter(isVolumeResult); // filter out models const dicomStore = useDICOMStore(); - const otherSegVolumesInStudy = pickOtherVolumesInStudy( + const otherSegVolumesInStudy = filterOtherVolumesInStudy( primaryDataSource.dataID, succeeded ).filter((ds) => { @@ -309,7 +311,7 @@ const useLoadDataStore = defineStore('loadData', () => { const [succeeded, errored] = partitionResults(results); if (!dataStore.primarySelection && succeeded.length) { - const primaryDataSource = pickBaseDataSource(succeeded); + const primaryDataSource = findBaseDataSource(succeeded); if (isVolumeResult(primaryDataSource)) { const selection = toDataSelection(primaryDataSource); dataStore.setPrimarySelection(selection); From 72c920cfb95ed5fc7f0577eb1093ce41c5f0c642 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Apr 2024 20:46:53 -0400 Subject: [PATCH 4/8] feat(configJson): add io.matchNames option --- .../content/doc/configuration_file.md | 18 +++++++++++++++++- src/io/import/configJson.ts | 3 +++ src/store/load-data.ts | 18 +++++++++++------- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/documentation/content/doc/configuration_file.md b/documentation/content/doc/configuration_file.md index af0e78a5..850d8390 100644 --- a/documentation/content/doc/configuration_file.md +++ b/documentation/content/doc/configuration_file.md @@ -49,10 +49,26 @@ VolView will include in the volview.zip file. } ``` -These are the supported file formats: +Working segment group file formats: hdf5, iwi.cbor, mha, nii, nii.gz, nrrd, vtk +## Automatic Segment Groups by File Name + +When loading files, VolView can automatically convert images to segment groups +if they follow this naming convention: An image with name like `foo.segmentation.bar` +will be converted to a segment group for a base image named like `foo.baz`. +The key is the `segmentation` extension to identify the segment group image. +Also the `matchNames` key must be set to `true`. The default is false. + +```json +{ + "io": { + "matchNames": true + } +} +``` + ## Keyboard Shortcuts Configure the keys to activate tools, change selected labels, and more. diff --git a/src/io/import/configJson.ts b/src/io/import/configJson.ts index 2f5c7e4e..293c6b4a 100644 --- a/src/io/import/configJson.ts +++ b/src/io/import/configJson.ts @@ -12,6 +12,7 @@ import { useViewStore } from '@/src/store/views'; import { actionToKey } from '@/src/composables/useKeyboardShortcuts'; import { useSegmentGroupStore } from '@/src/store/segmentGroups'; import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; +import useLoadDataStore from '@/src/store/load-data'; const layout = z .object({ @@ -59,6 +60,7 @@ const labels = z const io = z .object({ segmentGroupSaveFormat: z.string().optional(), + matchNames: z.boolean().optional(), }) .optional(); @@ -129,6 +131,7 @@ const applyIo = (manifest: Config) => { if (manifest.io.segmentGroupSaveFormat) useSegmentGroupStore().saveFormat = manifest.io.segmentGroupSaveFormat; + useLoadDataStore().matchNames = manifest.io.matchNames ?? false; }; export const applyConfig = (manifest: Config) => { diff --git a/src/store/load-data.ts b/src/store/load-data.ts index 96965751..6f8c7a6f 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -254,13 +254,14 @@ function loadLayers( // - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz function loadSegmentations( primaryDataSource: VolumeResult, - succeeded: Array> + succeeded: Array>, + matchNames: boolean ) { - const matchingNames = filterMatchingNames( - primaryDataSource, - succeeded, - 'segmentation' - ).filter(isVolumeResult); // filter out models + const matchingNames = matchNames + ? filterMatchingNames(primaryDataSource, succeeded, 'segmentation').filter( + isVolumeResult // filter out models + ) + : []; const dicomStore = useDICOMStore(); const otherSegVolumesInStudy = filterOtherVolumesInStudy( @@ -297,6 +298,8 @@ const useLoadDataStore = defineStore('loadData', () => { }; }; + const matchNames = ref(false); + const loadDataSources = wrapWithLoading(async (sources: DataSource[]) => { const dataStore = useDatasetStore(); @@ -316,7 +319,7 @@ const useLoadDataStore = defineStore('loadData', () => { const selection = toDataSelection(primaryDataSource); dataStore.setPrimarySelection(selection); loadLayers(primaryDataSource, succeeded); - loadSegmentations(primaryDataSource, succeeded); + loadSegmentations(primaryDataSource, succeeded, matchNames.value); } // then must be primaryDataSource.type === 'model' } @@ -340,6 +343,7 @@ const useLoadDataStore = defineStore('loadData', () => { return { isLoading, + matchNames, loadDataSources, }; }); From 10097fe5ed661aec4ae4fbf4bba6969e9c26eff5 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Apr 2024 21:48:05 -0400 Subject: [PATCH 5/8] fix(load-data): avoid circular import Moves loadDataSources logic to composable, keeps store mostly state. --- src/actions/loadUserFiles.ts | 264 ++++++++++++++++++++++++++++++++++- src/store/load-data.ts | 248 +------------------------------- 2 files changed, 264 insertions(+), 248 deletions(-) diff --git a/src/actions/loadUserFiles.ts b/src/actions/loadUserFiles.ts index 7e38464a..fe4662b9 100644 --- a/src/actions/loadUserFiles.ts +++ b/src/actions/loadUserFiles.ts @@ -1,9 +1,263 @@ -import { fileToDataSource, uriToDataSource } from '@/src/io/import/dataSource'; +import { UrlParams } from '@vueuse/core'; +import { + fileToDataSource, + uriToDataSource, + DataSource, + getDataSourceName, +} from '@/src/io/import/dataSource'; import useLoadDataStore from '@/src/store/load-data'; -import { wrapInArray } from '@/src/utils'; +import { useDatasetStore } from '@/src/store/datasets'; +import { useDICOMStore } from '@/src/store/datasets-dicom'; +import { useLayersStore } from '@/src/store/datasets-layers'; +import { useSegmentGroupStore } from '@/src/store/segmentGroups'; +import { wrapInArray, nonNullable } from '@/src/utils'; import { basename } from '@/src/utils/path'; import { parseUrl } from '@/src/utils/url'; -import { UrlParams } from '@vueuse/core'; +import { logError } from '@/src/utils/loggers'; +import { PipelineResultSuccess, partitionResults } from '@/src/core/pipeline'; +import { + ImportDataSourcesResult, + importDataSources, + toDataSelection, +} from '@/src/io/import/importDataSources'; +import { + ImportResult, + LoadableResult, + VolumeResult, + isLoadableResult, + isVolumeResult, +} from '@/src/io/import/common'; + +// higher value priority is preferred for picking a primary selection +const BASE_MODALITY_TYPES = { + CT: { priority: 3 }, + MR: { priority: 3 }, + US: { priority: 2 }, + DX: { priority: 1 }, +} as const; + +function findBaseDicom(loadableDataSources: Array) { + // find dicom dataset for primary selection if available + const dicoms = loadableDataSources.filter( + ({ dataType }) => dataType === 'dicom' + ); + // prefer some modalities as base + const dicomStore = useDICOMStore(); + const baseDicomVolumes = dicoms + .map((dicomSource) => { + const volumeInfo = dicomStore.volumeInfo[dicomSource.dataID]; + const modality = volumeInfo?.Modality as keyof typeof BASE_MODALITY_TYPES; + if (modality in BASE_MODALITY_TYPES) + return { + dicomSource, + priority: BASE_MODALITY_TYPES[modality]?.priority, + volumeInfo, + }; + return undefined; + }) + .filter(nonNullable) + .sort( + ( + { priority: a, volumeInfo: infoA }, + { priority: b, volumeInfo: infoB } + ) => { + const priorityDiff = a - b; + if (priorityDiff !== 0) return priorityDiff; + // same modality, then more slices preferred + if (!infoA.NumberOfSlices) return 1; + if (!infoB.NumberOfSlices) return -1; + return infoB.NumberOfSlices - infoA.NumberOfSlices; + } + ); + if (baseDicomVolumes.length) return baseDicomVolumes[0].dicomSource; + return undefined; +} + +// returns image and dicom sources, no config files +function filterLoadableDataSources( + succeeded: Array> +) { + return succeeded.flatMap((result) => { + return result.data.filter(isLoadableResult); + }); +} + +// Returns list of dataSources with file names where the name has the extension argument +// and the start of the file name matches the primary file name. +function filterMatchingNames( + primaryDataSource: VolumeResult, + succeeded: Array>, + extension: string +) { + const primaryName = getDataSourceName(primaryDataSource.dataSource); + if (!primaryName) return []; + const primaryNamePrefix = primaryName.split('.').slice(0, 1).join(); + return filterLoadableDataSources(succeeded) + .filter((ds) => ds !== primaryDataSource) + .map((importResult) => ({ + importResult, + name: getDataSourceName(importResult.dataSource), + })) + .filter(({ name }) => { + if (!name) return false; + const extensions = name.split('.').slice(1); + const hasExtension = extensions.includes(extension); + const nameMatchesPrimary = name.startsWith(primaryNamePrefix); + return hasExtension && nameMatchesPrimary; + }) + .map(({ importResult }) => importResult); +} + +function getStudyUID(volumeID: string) { + const dicomStore = useDICOMStore(); + const studyKey = dicomStore.volumeStudy[volumeID]; + return dicomStore.studyInfo[studyKey]?.StudyInstanceUID; +} + +function findBaseDataSource( + succeeded: Array> +) { + const loadableDataSources = filterLoadableDataSources(succeeded); + const baseDicom = findBaseDicom(loadableDataSources); + return baseDicom ?? loadableDataSources[0]; +} + +function filterOtherVolumesInStudy( + volumeID: string, + succeeded: Array> +) { + const targetStudyUID = getStudyUID(volumeID); + const dicomDataSources = filterLoadableDataSources(succeeded).filter( + ({ dataType }) => dataType === 'dicom' + ); + return dicomDataSources.filter((ds) => { + const sourceStudyUID = getStudyUID(ds.dataID); + return sourceStudyUID === targetStudyUID && ds.dataID !== volumeID; + }) as Array; +} + +// Layers a DICOM PET on a CT if found +function loadLayers( + primaryDataSource: VolumeResult, + succeeded: Array> +) { + if (primaryDataSource.dataType !== 'dicom') return; + const otherVolumesInStudy = filterOtherVolumesInStudy( + primaryDataSource.dataID, + succeeded + ); + const dicomStore = useDICOMStore(); + const primaryModality = + dicomStore.volumeInfo[primaryDataSource.dataID].Modality; + if (primaryModality !== 'CT') return; + // Look for one PET volume to layer with CT. Only one as there are often multiple "White Balance" corrected PET volumes. + const toLayer = otherVolumesInStudy.find((ds) => { + const otherModality = dicomStore.volumeInfo[ds.dataID].Modality; + return otherModality === 'PT'; + }); + if (!toLayer) return; + + const primarySelection = toDataSelection(primaryDataSource); + const layersStore = useLayersStore(); + const layerSelection = toDataSelection(toLayer); + layersStore.addLayer(primarySelection, layerSelection); +} + +// Loads other DataSources Segment Groups: +// - DICOM SEG modalities with matching StudyUIDs. +// - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz +function loadSegmentations( + primaryDataSource: VolumeResult, + succeeded: Array>, + matchNames: boolean +) { + const matchingNames = matchNames + ? filterMatchingNames(primaryDataSource, succeeded, 'segmentation').filter( + isVolumeResult // filter out models + ) + : []; + + const dicomStore = useDICOMStore(); + const otherSegVolumesInStudy = filterOtherVolumesInStudy( + primaryDataSource.dataID, + succeeded + ).filter((ds) => { + const modality = dicomStore.volumeInfo[ds.dataID].Modality; + if (!modality) return false; + return modality.trim() === 'SEG'; + }); + + const segmentGroupStore = useSegmentGroupStore(); + [...otherSegVolumesInStudy, ...matchingNames].forEach((ds) => { + const loadable = toDataSelection(ds); + segmentGroupStore.convertImageToLabelmap( + loadable, + toDataSelection(primaryDataSource) + ); + }); +} + +function loadDataSources(sources: DataSource[]) { + const load = async () => { + const loadDataStore = useLoadDataStore(); + const dataStore = useDatasetStore(); + + let results: ImportDataSourcesResult[]; + try { + results = await importDataSources(sources); + } catch (error) { + loadDataStore.setError(error as Error); + return; + } + + const [succeeded, errored] = partitionResults(results); + + if (!dataStore.primarySelection && succeeded.length) { + const primaryDataSource = findBaseDataSource(succeeded); + if (isVolumeResult(primaryDataSource)) { + const selection = toDataSelection(primaryDataSource); + dataStore.setPrimarySelection(selection); + loadLayers(primaryDataSource, succeeded); + loadSegmentations( + primaryDataSource, + succeeded, + loadDataStore.matchNames + ); + } // then must be primaryDataSource.type === 'model' + } + + if (errored.length) { + const errorMessages = errored.map((errResult) => { + // pick first error + const [firstError] = errResult.errors; + // pick innermost dataset that errored + const name = getDataSourceName(firstError.inputDataStackTrace[0]); + // log error for debugging + logError(firstError.cause); + return `- ${name}: ${firstError.message}`; + }); + const failedError = new Error( + `These files failed to load:\n${errorMessages.join('\n')}` + ); + + loadDataStore.setError(failedError); + } + }; + + const wrapWithLoading = void>(fn: T) => { + const { startLoading, stopLoading } = useLoadDataStore(); + return async function wrapper(...args: any[]) { + try { + startLoading(); + await fn(...args); + } finally { + stopLoading(); + } + }; + }; + + return wrapWithLoading(load)(); +} export function openFileDialog() { return new Promise((resolve) => { @@ -21,7 +275,7 @@ export function openFileDialog() { export async function loadFiles(files: File[]) { const dataSources = files.map(fileToDataSource); - return useLoadDataStore().loadDataSources(dataSources); + return loadDataSources(dataSources); } export async function loadUserPromptedFiles() { @@ -41,5 +295,5 @@ export async function loadUrls(params: UrlParams) { ) ); - return useLoadDataStore().loadDataSources(sources); + return loadDataSources(sources); } diff --git a/src/store/load-data.ts b/src/store/load-data.ts index 6f8c7a6f..bf924ed1 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -1,19 +1,3 @@ -import { PipelineResultSuccess, partitionResults } from '@/src/core/pipeline'; -import { DataSource, getDataSourceName } from '@/src/io/import/dataSource'; -import { - ImportDataSourcesResult, - importDataSources, - toDataSelection, -} from '@/src/io/import/importDataSources'; -import { - ImportResult, - LoadableResult, - VolumeResult, - isLoadableResult, - isVolumeResult, -} from '@/src/io/import/common'; -import { useDICOMStore } from '@/src/store/datasets-dicom'; -import { useDatasetStore } from '@/src/store/datasets'; import { useMessageStore } from '@/src/store/messages'; import { Maybe } from '@/src/types'; import { logError } from '@/src/utils/loggers'; @@ -22,17 +6,6 @@ import { computed, ref, watch } from 'vue'; import { useToast } from '@/src/composables/useToast'; import { TYPE } from 'vue-toastification'; import { ToastID, ToastOptions } from 'vue-toastification/dist/types/types'; -import { useLayersStore } from './datasets-layers'; -import { useSegmentGroupStore } from './segmentGroups'; -import { nonNullable } from '../utils'; - -// higher value priority is preferred for picking a primary selection -const BASE_MODALITY_TYPES = { - CT: { priority: 3 }, - MR: { priority: 3 }, - US: { priority: 2 }, - DX: { priority: 1 }, -} as const; const NotificationMessages = { Loading: 'Loading datasets...', @@ -47,7 +20,7 @@ const LoadingToastOptions = { closeOnClick: false, } satisfies ToastOptions; -function useLoadingNotifications() { +export function useLoadingNotifications() { const messageStore = useMessageStore(); const loadingCount = ref(0); @@ -122,229 +95,18 @@ function useLoadingNotifications() { }; } -function findBaseDicom(loadableDataSources: Array) { - // find dicom dataset for primary selection if available - const dicoms = loadableDataSources.filter( - ({ dataType }) => dataType === 'dicom' - ); - // prefer some modalities as base - const dicomStore = useDICOMStore(); - const baseDicomVolumes = dicoms - .map((dicomSource) => { - const volumeInfo = dicomStore.volumeInfo[dicomSource.dataID]; - const modality = volumeInfo?.Modality as keyof typeof BASE_MODALITY_TYPES; - if (modality in BASE_MODALITY_TYPES) - return { - dicomSource, - priority: BASE_MODALITY_TYPES[modality]?.priority, - volumeInfo, - }; - return undefined; - }) - .filter(nonNullable) - .sort( - ( - { priority: a, volumeInfo: infoA }, - { priority: b, volumeInfo: infoB } - ) => { - const priorityDiff = a - b; - if (priorityDiff !== 0) return priorityDiff; - // same modality, then more slices preferred - if (!infoA.NumberOfSlices) return 1; - if (!infoB.NumberOfSlices) return -1; - return infoB.NumberOfSlices - infoA.NumberOfSlices; - } - ); - if (baseDicomVolumes.length) return baseDicomVolumes[0].dicomSource; - return undefined; -} - -// returns image and dicom sources, no config files -function filterLoadableDataSources( - succeeded: Array> -) { - return succeeded.flatMap((result) => { - return result.data.filter(isLoadableResult); - }); -} - -// Returns list of dataSources with file names where the name has the extension argument -// and the start of the file name matches the primary file name. -function filterMatchingNames( - primaryDataSource: VolumeResult, - succeeded: Array>, - extension: string -) { - const primaryName = getDataSourceName(primaryDataSource.dataSource); - if (!primaryName) return []; - const primaryNamePrefix = primaryName.split('.').slice(0, 1).join(); - return filterLoadableDataSources(succeeded) - .filter((ds) => ds !== primaryDataSource) - .map((importResult) => ({ - importResult, - name: getDataSourceName(importResult.dataSource), - })) - .filter(({ name }) => { - if (!name) return false; - const extensions = name.split('.').slice(1); - const hasExtension = extensions.includes(extension); - const nameMatchesPrimary = name.startsWith(primaryNamePrefix); - return hasExtension && nameMatchesPrimary; - }) - .map(({ importResult }) => importResult); -} - -function getStudyUID(volumeID: string) { - const dicomStore = useDICOMStore(); - const studyKey = dicomStore.volumeStudy[volumeID]; - return dicomStore.studyInfo[studyKey]?.StudyInstanceUID; -} - -function findBaseDataSource( - succeeded: Array> -) { - const loadableDataSources = filterLoadableDataSources(succeeded); - const baseDicom = findBaseDicom(loadableDataSources); - return baseDicom ?? loadableDataSources[0]; -} - -function filterOtherVolumesInStudy( - volumeID: string, - succeeded: Array> -) { - const targetStudyUID = getStudyUID(volumeID); - const dicomDataSources = filterLoadableDataSources(succeeded).filter( - ({ dataType }) => dataType === 'dicom' - ); - return dicomDataSources.filter((ds) => { - const sourceStudyUID = getStudyUID(ds.dataID); - return sourceStudyUID === targetStudyUID && ds.dataID !== volumeID; - }) as Array; -} - -// Layers a DICOM PET on a CT if found -function loadLayers( - primaryDataSource: VolumeResult, - succeeded: Array> -) { - if (primaryDataSource.dataType !== 'dicom') return; - const otherVolumesInStudy = filterOtherVolumesInStudy( - primaryDataSource.dataID, - succeeded - ); - const dicomStore = useDICOMStore(); - const primaryModality = - dicomStore.volumeInfo[primaryDataSource.dataID].Modality; - if (primaryModality !== 'CT') return; - // Look for one PET volume to layer with CT. Only one as there are often multiple "White Balance" corrected PET volumes. - const toLayer = otherVolumesInStudy.find((ds) => { - const otherModality = dicomStore.volumeInfo[ds.dataID].Modality; - return otherModality === 'PT'; - }); - if (!toLayer) return; - - const primarySelection = toDataSelection(primaryDataSource); - const layersStore = useLayersStore(); - const layerSelection = toDataSelection(toLayer); - layersStore.addLayer(primarySelection, layerSelection); -} - -// Loads other DataSources Segment Groups: -// - DICOM SEG modalities with matching StudyUIDs. -// - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz -function loadSegmentations( - primaryDataSource: VolumeResult, - succeeded: Array>, - matchNames: boolean -) { - const matchingNames = matchNames - ? filterMatchingNames(primaryDataSource, succeeded, 'segmentation').filter( - isVolumeResult // filter out models - ) - : []; - - const dicomStore = useDICOMStore(); - const otherSegVolumesInStudy = filterOtherVolumesInStudy( - primaryDataSource.dataID, - succeeded - ).filter((ds) => { - const modality = dicomStore.volumeInfo[ds.dataID].Modality; - if (!modality) return false; - return modality.trim() === 'SEG'; - }); - - const segmentGroupStore = useSegmentGroupStore(); - [...otherSegVolumesInStudy, ...matchingNames].forEach((ds) => { - const loadable = toDataSelection(ds); - segmentGroupStore.convertImageToLabelmap( - loadable, - toDataSelection(primaryDataSource) - ); - }); -} - const useLoadDataStore = defineStore('loadData', () => { const { startLoading, stopLoading, setError, isLoading } = useLoadingNotifications(); - const wrapWithLoading = void>(fn: T) => { - return async function wrapper(...args: any[]) { - try { - startLoading(); - await fn(...args); - } finally { - stopLoading(); - } - }; - }; - const matchNames = ref(false); - const loadDataSources = wrapWithLoading(async (sources: DataSource[]) => { - const dataStore = useDatasetStore(); - - let results: ImportDataSourcesResult[]; - try { - results = await importDataSources(sources); - } catch (error) { - setError(error as Error); - return; - } - - const [succeeded, errored] = partitionResults(results); - - if (!dataStore.primarySelection && succeeded.length) { - const primaryDataSource = findBaseDataSource(succeeded); - if (isVolumeResult(primaryDataSource)) { - const selection = toDataSelection(primaryDataSource); - dataStore.setPrimarySelection(selection); - loadLayers(primaryDataSource, succeeded); - loadSegmentations(primaryDataSource, succeeded, matchNames.value); - } // then must be primaryDataSource.type === 'model' - } - - if (errored.length) { - const errorMessages = errored.map((errResult) => { - // pick first error - const [firstError] = errResult.errors; - // pick innermost dataset that errored - const name = getDataSourceName(firstError.inputDataStackTrace[0]); - // log error for debugging - logError(firstError.cause); - return `- ${name}: ${firstError.message}`; - }); - const failedError = new Error( - `These files failed to load:\n${errorMessages.join('\n')}` - ); - - setError(failedError); - } - }); - return { - isLoading, matchNames, - loadDataSources, + isLoading, + startLoading, + stopLoading, + setError, }; }); From 6d2ea5090b107840a32143c296115ce260b8d8df Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Apr 2024 22:54:40 -0400 Subject: [PATCH 6/8] fix(loadUserFiles): no named segmentation image as primary --- src/actions/loadUserFiles.ts | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/actions/loadUserFiles.ts b/src/actions/loadUserFiles.ts index fe4662b9..dddebb15 100644 --- a/src/actions/loadUserFiles.ts +++ b/src/actions/loadUserFiles.ts @@ -36,6 +36,8 @@ const BASE_MODALITY_TYPES = { DX: { priority: 1 }, } as const; +const SEGMENTATION_EXTENSION = 'segmentation'; + function findBaseDicom(loadableDataSources: Array) { // find dicom dataset for primary selection if available const dicoms = loadableDataSources.filter( @@ -73,6 +75,25 @@ function findBaseDicom(loadableDataSources: Array) { return undefined; } +function isSegmentation(extension: string, name: string) { + const extensions = name.split('.').slice(1); + return extensions.includes(extension); +} + +// does not pick segmentation images +function findBaseImage(loadableDataSources: Array) { + const baseImages = loadableDataSources + .filter(({ dataType }) => dataType === 'image') + .filter((importResult) => { + const name = getDataSourceName(importResult.dataSource); + if (!name) return false; + return !isSegmentation(SEGMENTATION_EXTENSION, name); + }); + + if (baseImages.length) return baseImages[0]; + return undefined; +} + // returns image and dicom sources, no config files function filterLoadableDataSources( succeeded: Array> @@ -100,8 +121,7 @@ function filterMatchingNames( })) .filter(({ name }) => { if (!name) return false; - const extensions = name.split('.').slice(1); - const hasExtension = extensions.includes(extension); + const hasExtension = isSegmentation(extension, name); const nameMatchesPrimary = name.startsWith(primaryNamePrefix); return hasExtension && nameMatchesPrimary; }) @@ -119,7 +139,10 @@ function findBaseDataSource( ) { const loadableDataSources = filterLoadableDataSources(succeeded); const baseDicom = findBaseDicom(loadableDataSources); - return baseDicom ?? loadableDataSources[0]; + if (baseDicom) return baseDicom; + const baseImage = findBaseImage(loadableDataSources); + if (baseImage) return baseImage; + return loadableDataSources[0]; } function filterOtherVolumesInStudy( @@ -172,7 +195,11 @@ function loadSegmentations( matchNames: boolean ) { const matchingNames = matchNames - ? filterMatchingNames(primaryDataSource, succeeded, 'segmentation').filter( + ? filterMatchingNames( + primaryDataSource, + succeeded, + SEGMENTATION_EXTENSION + ).filter( isVolumeResult // filter out models ) : []; From 73c7bf8656cf079a4e6fe74d535399afa2b4b08c Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 29 Apr 2024 10:56:11 -0400 Subject: [PATCH 7/8] feat(configJson): configurable extension for auto seg group --- .../content/doc/configuration_file.md | 10 +++-- src/actions/loadUserFiles.ts | 44 +++++++++++-------- src/io/import/configJson.ts | 10 ++++- src/store/datasets-layers.ts | 15 +------ src/store/load-data.ts | 4 +- 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/documentation/content/doc/configuration_file.md b/documentation/content/doc/configuration_file.md index 850d8390..73debcc2 100644 --- a/documentation/content/doc/configuration_file.md +++ b/documentation/content/doc/configuration_file.md @@ -56,15 +56,17 @@ hdf5, iwi.cbor, mha, nii, nii.gz, nrrd, vtk ## Automatic Segment Groups by File Name When loading files, VolView can automatically convert images to segment groups -if they follow this naming convention: An image with name like `foo.segmentation.bar` +if they follow a naming convention. For example, an image with name like `foo.segmentation.bar` will be converted to a segment group for a base image named like `foo.baz`. -The key is the `segmentation` extension to identify the segment group image. -Also the `matchNames` key must be set to `true`. The default is false. +The `segmentation` extension is defined by the `io.segmentGroupExtension` key, which takes a +string. Files `foo.[segmentGroupExtension].bar` will be automatilly converted to segment groups for a base image named `foo.baz`. The default is `''` and will disable the feature. + +This will define `myFile.seg.nrrd` as a segment group for a `myFile.nii` base file. ```json { "io": { - "matchNames": true + "segmentGroupExtension": "seg" } } ``` diff --git a/src/actions/loadUserFiles.ts b/src/actions/loadUserFiles.ts index dddebb15..351b1447 100644 --- a/src/actions/loadUserFiles.ts +++ b/src/actions/loadUserFiles.ts @@ -36,8 +36,6 @@ const BASE_MODALITY_TYPES = { DX: { priority: 1 }, } as const; -const SEGMENTATION_EXTENSION = 'segmentation'; - function findBaseDicom(loadableDataSources: Array) { // find dicom dataset for primary selection if available const dicoms = loadableDataSources.filter( @@ -76,18 +74,22 @@ function findBaseDicom(loadableDataSources: Array) { } function isSegmentation(extension: string, name: string) { + if (!extension) return false; // avoid 'foo..bar' if extension is '' const extensions = name.split('.').slice(1); return extensions.includes(extension); } // does not pick segmentation images -function findBaseImage(loadableDataSources: Array) { +function findBaseImage( + loadableDataSources: Array, + segmentGroupExtension: string +) { const baseImages = loadableDataSources .filter(({ dataType }) => dataType === 'image') .filter((importResult) => { const name = getDataSourceName(importResult.dataSource); if (!name) return false; - return !isSegmentation(SEGMENTATION_EXTENSION, name); + return !isSegmentation(segmentGroupExtension, name); }); if (baseImages.length) return baseImages[0]; @@ -135,12 +137,14 @@ function getStudyUID(volumeID: string) { } function findBaseDataSource( - succeeded: Array> + succeeded: Array>, + segmentGroupExtension: string ) { const loadableDataSources = filterLoadableDataSources(succeeded); const baseDicom = findBaseDicom(loadableDataSources); if (baseDicom) return baseDicom; - const baseImage = findBaseImage(loadableDataSources); + + const baseImage = findBaseImage(loadableDataSources, segmentGroupExtension); if (baseImage) return baseImage; return loadableDataSources[0]; } @@ -186,23 +190,21 @@ function loadLayers( layersStore.addLayer(primarySelection, layerSelection); } -// Loads other DataSources Segment Groups: +// Loads other DataSources as Segment Groups: // - DICOM SEG modalities with matching StudyUIDs. // - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz function loadSegmentations( primaryDataSource: VolumeResult, succeeded: Array>, - matchNames: boolean + segmentGroupExtension: string ) { - const matchingNames = matchNames - ? filterMatchingNames( - primaryDataSource, - succeeded, - SEGMENTATION_EXTENSION - ).filter( - isVolumeResult // filter out models - ) - : []; + const matchingNames = filterMatchingNames( + primaryDataSource, + succeeded, + segmentGroupExtension + ).filter( + isVolumeResult // filter out models + ); const dicomStore = useDICOMStore(); const otherSegVolumesInStudy = filterOtherVolumesInStudy( @@ -240,7 +242,11 @@ function loadDataSources(sources: DataSource[]) { const [succeeded, errored] = partitionResults(results); if (!dataStore.primarySelection && succeeded.length) { - const primaryDataSource = findBaseDataSource(succeeded); + const primaryDataSource = findBaseDataSource( + succeeded, + loadDataStore.segmentGroupExtension + ); + if (isVolumeResult(primaryDataSource)) { const selection = toDataSelection(primaryDataSource); dataStore.setPrimarySelection(selection); @@ -248,7 +254,7 @@ function loadDataSources(sources: DataSource[]) { loadSegmentations( primaryDataSource, succeeded, - loadDataStore.matchNames + loadDataStore.segmentGroupExtension ); } // then must be primaryDataSource.type === 'model' } diff --git a/src/io/import/configJson.ts b/src/io/import/configJson.ts index 293c6b4a..9ac10ee9 100644 --- a/src/io/import/configJson.ts +++ b/src/io/import/configJson.ts @@ -14,6 +14,9 @@ import { useSegmentGroupStore } from '@/src/store/segmentGroups'; import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; import useLoadDataStore from '@/src/store/load-data'; +// -------------------------------------------------------------------------- +// Interface + const layout = z .object({ activeLayout: zodEnumFromObjKeys(Layouts).optional(), @@ -57,10 +60,13 @@ const labels = z }) .optional(); +// -------------------------------------------------------------------------- +// IO + const io = z .object({ segmentGroupSaveFormat: z.string().optional(), - matchNames: z.boolean().optional(), + segmentGroupExtension: z.string().default(''), }) .optional(); @@ -131,7 +137,7 @@ const applyIo = (manifest: Config) => { if (manifest.io.segmentGroupSaveFormat) useSegmentGroupStore().saveFormat = manifest.io.segmentGroupSaveFormat; - useLoadDataStore().matchNames = manifest.io.matchNames ?? false; + useLoadDataStore().segmentGroupExtension = manifest.io.segmentGroupExtension; }; export const applyConfig = (manifest: Config) => { diff --git a/src/store/datasets-layers.ts b/src/store/datasets-layers.ts index e8eeba47..6293cb54 100644 --- a/src/store/datasets-layers.ts +++ b/src/store/datasets-layers.ts @@ -1,9 +1,7 @@ import { ref } from 'vue'; -import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import { defineStore } from 'pinia'; -import { compareImageSpaces } from '@/src/utils/imageSpace'; import { DataSelection, getImage, @@ -11,7 +9,7 @@ import { makeImageSelection, selectionEquals, } from '@/src/utils/dataSelection'; -import { resample } from '../io/resample/resample'; +import { ensureSameSpace } from '@/src/io/resample/resample'; import { useErrorMessage } from '../composables/useErrorMessage'; import { Manifest, StateFile } from '../io/state-file/schema'; @@ -89,16 +87,7 @@ export const useLayersStore = defineStore('layer', () => { ); } - let image: vtkImageData; - if (compareImageSpaces(parentImage, sourceImage)) { - image = sourceImage; - } else { - const itkImage = await resample( - vtkITKHelper.convertVtkToItkImage(parentImage), - vtkITKHelper.convertVtkToItkImage(sourceImage) - ); - image = vtkITKHelper.convertItkToVtkImage(itkImage); - } + const image = await ensureSameSpace(parentImage, sourceImage); this.layerImages[id] = image; } diff --git a/src/store/load-data.ts b/src/store/load-data.ts index bf924ed1..946627bb 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -99,10 +99,10 @@ const useLoadDataStore = defineStore('loadData', () => { const { startLoading, stopLoading, setError, isLoading } = useLoadingNotifications(); - const matchNames = ref(false); + const segmentGroupExtension = ref(''); return { - matchNames, + segmentGroupExtension, isLoading, startLoading, stopLoading, From afa82535a6030972e46f0e82cb818183020c8c94 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 30 Apr 2024 14:20:56 -0400 Subject: [PATCH 8/8] chore(deps): bump chromedriver version --- package-lock.json | 201 ++++++++++++++++++++++++++++++++++++---------- package.json | 4 +- 2 files changed, 162 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 221882d3..81fc805b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "chai-almost": "^1.0.1", "chai-as-promised": "7.1.1", "chai-subset": "^1.6.0", - "chromedriver": "^121.0.2", + "chromedriver": "^124.0.1", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^7.32.0", @@ -8011,17 +8011,17 @@ } }, "node_modules/chromedriver": { - "version": "121.0.2", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz", - "integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==", + "version": "124.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz", + "integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.5", + "axios": "^1.6.7", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", - "https-proxy-agent": "^5.0.1", + "proxy-agent": "^6.4.0", "proxy-from-env": "^1.1.0", "tcp-port-used": "^1.0.2" }, @@ -8032,6 +8032,72 @@ "node": ">=18" } }, + "node_modules/chromedriver/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/chromedriver/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/chromium-bidi": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", @@ -16521,9 +16587,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.0.tgz", - "integrity": "sha512-t4tRAMx0uphnZrio0S0Jw9zg3oDbz1zVhQ/Vy18FjLfP1XOLNUEjaVxYCYRI6NS+BsMBXKIzV6cTLOkO9AtywA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", "dev": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -16531,9 +16597,9 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" @@ -16565,9 +16631,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", - "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -18926,12 +18992,12 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", - "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "dev": true, "dependencies": { - "agent-base": "^7.0.1", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -18940,9 +19006,9 @@ } }, "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -28278,18 +28344,71 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "chromedriver": { - "version": "121.0.2", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz", - "integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==", + "version": "124.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz", + "integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.5", + "axios": "^1.6.7", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", - "https-proxy-agent": "^5.0.1", + "proxy-agent": "^6.4.0", "proxy-from-env": "^1.1.0", "tcp-port-used": "^1.0.2" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + } } }, "chromium-bidi": { @@ -34644,9 +34763,9 @@ "devOptional": true }, "pac-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.0.tgz", - "integrity": "sha512-t4tRAMx0uphnZrio0S0Jw9zg3oDbz1zVhQ/Vy18FjLfP1XOLNUEjaVxYCYRI6NS+BsMBXKIzV6cTLOkO9AtywA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", "dev": true, "requires": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -34654,9 +34773,9 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "dependencies": { "agent-base": { @@ -34679,9 +34798,9 @@ } }, "https-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", - "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "requires": { "agent-base": "^7.0.2", @@ -36466,20 +36585,20 @@ } }, "socks-proxy-agent": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", - "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "dev": true, "requires": { - "agent-base": "^7.0.1", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, "dependencies": { "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "requires": { "debug": "^4.3.4" diff --git a/package.json b/package.json index 8aaf7b25..d57ef2fa 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "chai-almost": "^1.0.1", "chai-as-promised": "7.1.1", "chai-subset": "^1.6.0", - "chromedriver": "^121.0.2", + "chromedriver": "^124.0.1", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^7.32.0", @@ -126,4 +126,4 @@ "eslint" ] } -} \ No newline at end of file +}