Skip to content

Commit

Permalink
fix(importDataSources): set aside config.JSONs for later application
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Oct 26, 2023
1 parent c4a763c commit e64df9c
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 268 deletions.
2 changes: 2 additions & 0 deletions src/io/import/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { DataSource, FileSource } from '@/src/io/import/dataSource';
import { Handler } from '@/src/core/pipeline';
import { ARCHIVE_FILE_TYPES } from '@/src/io/mimeTypes';
import { Awaitable } from '@vueuse/core';
import { Config } from '@/src/io/import/configSchema';

export interface ImportResult {
dataSource: DataSource;
dataID?: string;
dataType?: 'image' | 'dicom' | 'model';
config?: Config;
}

export type ArchiveContents = Record<string, File>;
Expand Down
115 changes: 115 additions & 0 deletions src/io/import/configSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { z } from 'zod';
import { zodEnumFromObjKeys } from '@/src/utils';
import { ACTIONS } from '@/src/constants';
import { Layouts } from '@/src/config';

// for applyConfig
import { useRectangleStore } from '@/src/store/tools/rectangles';
import { useRulerStore } from '@/src/store/tools/rulers';
import { useDataBrowserStore } from '@/src/store/data-browser';
import { usePolygonStore } from '@/src/store/tools/polygons';
import { useViewStore } from '@/src/store/views';
import { actionToKey } from '@/src/composables/useKeyboardShortcuts';

const layout = z
.object({
activeLayout: zodEnumFromObjKeys(Layouts).optional(),
})
.optional();

const dataBrowser = z
.object({
hideSampleData: z.boolean().optional(),
})
.optional();

const shortcuts = z.record(zodEnumFromObjKeys(ACTIONS), z.string()).optional();

// --------------------------------------------------------------------------
// Labels

const color = z.string();

const label = z.object({
color,
strokeWidth: z.number().optional(),
});

const rulerLabel = label;
const polygonLabel = label;

const rectangleLabel = z.intersection(
label,
z.object({
fillColor: color,
})
);

const labels = z
.object({
defaultLabels: z.record(label).or(z.null()).optional(),
rulerLabels: z.record(rulerLabel).or(z.null()).optional(),
rectangleLabels: z.record(rectangleLabel).or(z.null()).optional(),
polygonLabels: z.record(polygonLabel).or(z.null()).optional(),
})
.optional();

export const config = z.object({
layout,
dataBrowser,
labels,
shortcuts,
});

export type Config = z.infer<typeof config>;

export const readConfigFile = async (configFile: File) => {
const decoder = new TextDecoder();
const ab = await configFile.arrayBuffer();
const text = decoder.decode(new Uint8Array(ab));
return config.parse(JSON.parse(text));
};

const applyLabels = (manifest: Config) => {
if (!manifest.labels) return;

// pass through null labels, use fallback labels if undefined
const labelsIfUndefined = (
toolLabels: (typeof manifest.labels)[keyof typeof manifest.labels]
) => {
if (toolLabels === undefined) return manifest.labels?.defaultLabels;
return toolLabels;
};

const { rulerLabels, rectangleLabels, polygonLabels } = manifest.labels;
useRulerStore().mergeLabels(labelsIfUndefined(rulerLabels));
useRectangleStore().mergeLabels(labelsIfUndefined(rectangleLabels));
usePolygonStore().mergeLabels(labelsIfUndefined(polygonLabels));
};

const applySampleData = (manifest: Config) => {
useDataBrowserStore().hideSampleData = !!manifest.dataBrowser?.hideSampleData;
};

const applyLayout = (manifest: Config) => {
if (manifest.layout?.activeLayout) {
const startingLayout = Layouts[manifest.layout.activeLayout];
useViewStore().setLayout(startingLayout);
}
};

const applyShortcuts = (manifest: Config) => {
if (!manifest.shortcuts) return;

actionToKey.value = {
...actionToKey.value,
...manifest.shortcuts,
};
};

export const applyConfig = (manifest: Config) => {
applyLayout(manifest);
applyLabels(manifest);
applySampleData(manifest);
applyShortcuts(manifest);
};
154 changes: 59 additions & 95 deletions src/io/import/importDataSources.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import Pipeline, {
PipelineResult,
PipelineResultError,
PipelineResultSuccess,
partitionResults,
} from '@/src/core/pipeline';
import { ImportHandler, ImportResult } from '@/src/io/import/common';
import Pipeline from '@/src/core/pipeline';
import { ImportHandler } from '@/src/io/import/common';
import { DataSource, DataSourceWithFile } from '@/src/io/import/dataSource';
import { nonNullable } from '@/src/utils';
import handleDicomFile from '@/src/io/import/processors/handleDicomFile';
import downloadUrl from '@/src/io/import/processors/downloadUrl';
import extractArchive from '@/src/io/import/processors/extractArchive';
Expand All @@ -19,13 +15,7 @@ import updateFileMimeType from '@/src/io/import/processors/updateFileMimeType';
import handleConfig from '@/src/io/import/processors/handleConfig';
import { useDICOMStore } from '@/src/store/datasets-dicom';
import { makeDICOMSelection, makeImageSelection } from '@/src/store/datasets';
import {
EARLIEST_PRIORITY,
earliestPriority,
prioritizeJSON,
prioritizeStateFile,
PriorityResult,
} from './processors/prioritize';
import { applyConfig } from '@/src/io/import/configSchema';

/**
* Tries to turn a thrown object into a meaningful error string.
Expand All @@ -50,92 +40,57 @@ export async function importDataSources(dataSources: DataSource[]) {
dicomDataSources: [] as DataSourceWithFile[],
};

const prioritizes = [
const middleware = [
// updating the file type should be first in the pipeline
updateFileMimeType,
// before extractArchive as .zip file is part of state file check
prioritizeStateFile,

// source generators
// before extractArchive as .zip extension is part of state file check
restoreStateFile,
handleRemoteManifest,
handleGoogleCloudStorage,
handleAmazonS3,
downloadUrl,
extractArchiveTargetFromCache,
extractArchive,

// sinks
prioritizeJSON,
earliestPriority,
];
const prioritizer = new Pipeline<
DataSource,
PriorityResult | ImportResult,
typeof importContext
>(prioritizes);

const priorityOrImportResult = await Promise.all(
dataSources.map((r) => prioritizer.execute(r, importContext))
);

const [fetchSuccess, errorOnDataFetches] = partitionResults(
priorityOrImportResult
);

const withPriority = fetchSuccess
.flatMap((r) => r.data)
.map((importOrPriority) => {
return {
dataSource: importOrPriority.dataSource,
priority:
'priority' in importOrPriority
? importOrPriority.priority
: EARLIEST_PRIORITY,
};
});

// Group same priorities, lowest priority first
const priorityBuckets = withPriority.reduce((buckets, result) => {
const { priority = 0 } = result;
if (!buckets[priority]) {
// eslint-disable-next-line no-param-reassign
buckets[priority] = [];
}
buckets[priority].push(result.dataSource);
return buckets;
}, [] as Array<Array<DataSource>>);

const loaders = [
restoreStateFile,
handleConfig,
handleConfig, // collect to apply later
// should be before importSingleFile, since DICOM is more specific
handleDicomFile,
importSingleFile,
// catch any unhandled resource
unhandledResource,
];
const fileLoader = new Pipeline(loaders);
const loader = new Pipeline(middleware);

// Serially processes buckets by priority.
// Configuration in later priority buckets will override earlier ones.
const results = [] as Array<PipelineResult<DataSource, ImportResult>>;
for (let i = 0; i < priorityBuckets.length; i++) {
const sources = priorityBuckets[i];
// sparse array, so check for undefined
if (sources) {
// eslint-disable-next-line no-await-in-loop
const bucketResults = await Promise.all(
sources.map((dataSource) =>
fileLoader.execute(dataSource, importContext)
const results = await Promise.all(
dataSources.map((r) => loader.execute(r, importContext))
);

// Apply config.JSONs
const configResult = await (async () => {
try {
results
.flatMap((pipelineResult) =>
pipelineResult.ok ? pipelineResult.data : []
)
);
results.push(...bucketResults);
.map((result) => result.config)
.filter(nonNullable)
.forEach(applyConfig);
return {
ok: true as const,
data: [],
};
} catch (err) {
return {
ok: false as const,
errors: [
{
message: toMeaningfulErrorString(err),
cause: err,
inputDataStackTrace: [],
},
],
};
}
}

if (!importContext.dicomDataSources.length) {
return [...errorOnDataFetches, ...results];
}
})();

// handle DICOM loading
const dicomDataSource: DataSource = {
Expand All @@ -145,37 +100,46 @@ export async function importDataSources(dataSources: DataSource[]) {
};
const dicomResult = await (async () => {
try {
if (!importContext.dicomDataSources.length) {
return {
ok: true as const,
data: [],
};
}
const volumeKeys = await useDICOMStore().importFiles(
importContext.dicomDataSources
);
return {
ok: true,
data: volumeKeys.map((key) => ({
dataID: key,
dataType: 'dicom' as const,
dataSource: dicomDataSource,
})),
} as PipelineResultSuccess<ImportResult>;
ok: true as const,
data: volumeKeys.map(
(key) =>
({
dataID: key,
dataType: 'dicom' as const,
dataSource: dicomDataSource,
} as const)
),
};
} catch (err) {
return {
ok: false,
ok: false as const,
errors: [
{
message: toMeaningfulErrorString(err),
cause: err,
inputDataStackTrace: [dicomDataSource],
},
],
} as PipelineResultError<typeof dicomDataSource>;
};
}
})();

return [
...errorOnDataFetches,
// remove all results that have no result data
...results.filter((result) => !result.ok || result.data.length),
...results,
dicomResult,
];
configResult,
].filter((result) => !result.ok || (result.ok && result.data.length));
}

export type ImportDataSourcesResult = Awaited<
Expand Down
Loading

0 comments on commit e64df9c

Please sign in to comment.