diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js index 31b8f7431e..65250cfb7b 100644 --- a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js @@ -1,27 +1,21 @@ import asyncStates from '@zooniverse/async-states' import PropTypes from 'prop-types' import { useTranslation } from '@translations/i18n' -import { lazy, Suspense } from 'react' - import { withStores } from '@helpers' import getViewer from './helpers/getViewer' -const VolumetricViewer = lazy(() => import('@zooniverse/subject-viewers/VolumetricViewer')) - function storeMapper(classifierStore) { const { subjects: { active: subject, loadingState: subjectQueueState }, subjectViewer: { onSubjectReady, onError, loadingState: subjectReadyState }, - projects: { active: project } } = classifierStore const drawingTasks = classifierStore?.workflowSteps.findTasksByType('drawing') const transcriptionTasks = classifierStore?.workflowSteps.findTasksByType('transcription') const enableInteractionLayer = drawingTasks.length > 0 || transcriptionTasks.length > 0 - + return { enableInteractionLayer, - isVolumetricViewer: project?.isVolumetricViewer ?? false, onError, onSubjectReady, subject, @@ -32,7 +26,6 @@ function storeMapper(classifierStore) { function SubjectViewer({ enableInteractionLayer, - isVolumetricViewer, onError, onSubjectReady, subject, @@ -53,23 +46,19 @@ function SubjectViewer({ return null } case asyncStates.success: { - const Viewer = (isVolumetricViewer) - ? VolumetricViewer - : getViewer(subject?.viewer) + const Viewer = getViewer(subject?.viewer) if (Viewer) { return ( - Suspense boundary

}> - -
+ ) } diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js index 35ac7a0dac..41ba788a66 100644 --- a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js @@ -39,20 +39,6 @@ describe('Component > SubjectViewer', function () { expect(screen.getByLabelText('Subject 1234')).to.exist() }) - it('should render the VolumetricViewer if isVolumetricViewer = true', async function () { - render() - expect(screen.getByText('Suspense boundary')).to.exist() - expect(await screen.findByTestId('subject-viewer-volumetric')).to.exist() - }) - describe('when there is an null viewer because of invalid subject media', function () { it('should render null', function () { const { container } = render( diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/README.md b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/README.md new file mode 100644 index 0000000000..5e8cdb0251 --- /dev/null +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/README.md @@ -0,0 +1,3 @@ +# Volumetric Viewer + +Implementation of the `lib-subject-viewers/VolumetricViewer` component. diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricMockSubject.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricMockSubject.js new file mode 100644 index 0000000000..e653b6d17b --- /dev/null +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricMockSubject.js @@ -0,0 +1,9 @@ +export const VolumetricSubjectMock = { + id: 'mockSubject', + locations: [ + { + 'application/json': 'https://zooniverse.org/subject.json' + }, + ], + subjectJSON: 'GRnI+hnIr5bIr5Z9r5Z9+uHIr5bIr5Z9r5Z9ZJZ9ZEv6r5Z9r5Z9ZJZ9ZEt9ZEsyGZZ9GRl9ZEt9ZEsyGUsyGQ==' +} diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.spec.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.spec.js new file mode 100644 index 0000000000..60420e7e84 --- /dev/null +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.spec.js @@ -0,0 +1,19 @@ +import { render, screen } from '@testing-library/react' +import { expect } from 'chai' +import asyncStates from '@zooniverse/async-states' +import VolumetricViewerWrapper from './VolumetricViewerWrapper' + +describe('Component > VolumetricViewer', function () { + it('should render the Volumetric Viewer asynchronously', async function () { + render() + expect(screen.getByText('Suspense boundary')).to.exist() + expect(await screen.findByTestId('subject-viewer-volumetric')).to.exist() + }) +}) diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.stories.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.stories.js new file mode 100644 index 0000000000..1aefad7d8e --- /dev/null +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewer.stories.js @@ -0,0 +1,17 @@ +import asyncStates from '@zooniverse/async-states' +import { VolumetricViewerWrapper } from './VolumetricViewerWrapper' +import { VolumetricSubjectMock } from './VolumetricMockSubject' + +export default { + title: 'Subject Viewers / VolumetricViewer', + component: VolumetricViewerWrapper +} + +export function Default() { + return ( + + ) +} diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewerWrapper.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewerWrapper.js new file mode 100644 index 0000000000..a90d9e244f --- /dev/null +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/VolumetricViewer/VolumetricViewerWrapper.js @@ -0,0 +1,35 @@ +import asyncStates from '@zooniverse/async-states' +import { lazy, Suspense } from 'react' +import { MobXProviderContext } from 'mobx-react' +import { useContext } from 'react' + +const VolumetricViewer = lazy(() => import('@zooniverse/subject-viewers/VolumetricViewer')) +const DEFAULT_HANDLER = () => {} + +function VolumetricViewerWrapper({ + loadingState = asyncStates.initialized, + onError = DEFAULT_HANDLER, + onReady = DEFAULT_HANDLER, + subject, +}) { + const stores = useContext(MobXProviderContext) + const addAnnotation = stores?.classifierStore?.classifications?.addAnnotation ?? DEFAULT_HANDLER + const activeStepTasks = stores?.classifierStore?.workflowSteps?.activeStepTasks ?? [] + + function onAnnotationUpdate(annotations) { + if (activeStepTasks[0]) + addAnnotation(activeStepTasks[0], annotations) + } + + return Suspense boundary

}> + +
+} + +export default VolumetricViewerWrapper diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/helpers/getViewer/getViewer.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/helpers/getViewer/getViewer.js index 5be32db460..1012b4b724 100644 --- a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/helpers/getViewer/getViewer.js +++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/helpers/getViewer/getViewer.js @@ -7,6 +7,7 @@ import SingleImageViewer from '../../components/SingleImageViewer' import SingleTextViewer from '../../components/SingleTextViewer' import SingleVideoViewer from '../../components/SingleVideoViewer' import SubjectGroupViewer from '../../components/SubjectGroupViewer' +import VolumetricViewer from '../../components/VolumetricViewer/VolumetricViewerWrapper' const viewers = { dataImage: DataImageViewer, @@ -20,7 +21,8 @@ const viewers = { singleText: SingleTextViewer, singleVideo: SingleVideoViewer, subjectGroup: SubjectGroupViewer, - variableStar: JSONDataViewer + variableStar: JSONDataViewer, + volumetric: VolumetricViewer } function getViewer (viewer) { diff --git a/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.js b/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.js index 481dfb07ca..fedfa88e49 100644 --- a/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.js +++ b/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.js @@ -55,12 +55,16 @@ Object.defineProperty(subjectViewers, 'subjectGroup', { enumerable: true }) - Object.defineProperty(subjectViewers, 'variableStar', { value: 'variableStar', enumerable: true }) +Object.defineProperty(subjectViewers, 'volumetric', { + value: 'volumetric', + enumerable: true +}) + // helper for returning subject viewers (e.g. for use in MST enumerable type) Object.defineProperty(subjectViewers, 'values', { value: Object.values(subjectViewers) diff --git a/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.spec.js b/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.spec.js index 4114a87ee7..e6245ee20a 100644 --- a/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.spec.js +++ b/packages/lib-classifier/src/helpers/subjectViewers/subjectViewers.spec.js @@ -13,7 +13,8 @@ describe('Helpers > subjectViewers', function () { 'singleText', 'singleVideo', 'subjectGroup', - 'variableStar' + 'variableStar', + 'volumetric' ] viewers.forEach(function (viewer) { diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js index 8be639c1c0..818338a771 100644 --- a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js @@ -1,14 +1,12 @@ +import { bool, shape, string } from "prop-types" import { Box, Text } from "grommet" import { Blank } from "grommet-icons" import InputStatus from "../../../components/InputStatus" import { Markdownz } from "@zooniverse/react-components" import { observer } from "mobx-react" -import { bool, shape, string } from "prop-types" import styled from "styled-components" import TaskInput from "../../../components/TaskInput" -// Note: ANNOTATION_COUNT will be refactored in next PR to use MobX Annotations -const ANNOTATION_COUNT = 3 const SVG_ARROW = "48 50, 48 15, 40 15, 50 0, 60 15, 52 15, 52 50" const StyledInstructionText = styled(Text)` @@ -33,7 +31,11 @@ const StyledToolIcon = styled.div` } ` -function VolumetricTask({ disabled = false, task }) { +function VolumetricTask({ + annotation, + disabled = false, + task +}) { return ( @@ -71,7 +73,7 @@ function VolumetricTask({ disabled = false, task }) { } - labelStatus={} + labelStatus={} name="volumetric-tool" type="radio" /> diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js index 7587fa958d..426802df4e 100644 --- a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js @@ -1,10 +1,22 @@ import { types } from 'mobx-state-tree' import Annotation from '../../../models/Annotation' +const PointsModel = types.model('PointsModel', { + active: types.array(types.number), + connected: types.array(types.array(types.number), []), + all: types.array(types.number), +}); + +const AnnotationModel = types.model('AnnotationModel', { + label: types.string, + threshold: types.number, + points: PointsModel, +}); + const Volumetric = types .model('Volumetric', { taskType: types.literal('volumetric'), - value: types.optional(types.string, ''), + value: types.array(AnnotationModel) }) const VolumetricAnnotation = types.compose('VolumetricAnnotation', Annotation, Volumetric) diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js index dbc2d3b7e4..512787c226 100644 --- a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js @@ -19,6 +19,16 @@ describe("Model > VolumetricTask", function () { type: "single", } + const mockAnnotationsSnapshot = [{ + label: `Test Annotation`, + threshold: 15, + points: { + active: [], + connected: [], + all: [] + } + }] + it("should exist", function () { const task = VolumetricTask.TaskModel.create(volumetricTask) expect(task).to.be.ok() @@ -65,13 +75,13 @@ describe("Model > VolumetricTask", function () { annotation = task.defaultAnnotation() }) - it("should start up with an empty string", function () { - expect(annotation.value).to.equal("") + it("should start up with an empty array", function () { + expect(annotation.value).to.deep.equal([]) }) it("should update annotations", function () { - annotation.update("Hello there!") - expect(annotation.value).to.equal("Hello there!") + annotation.update(mockAnnotationsSnapshot); + expect(annotation.value).to.deep.equal(mockAnnotationsSnapshot) }) }) }) diff --git a/packages/lib-classifier/src/store/subjects/Subject/Subject.js b/packages/lib-classifier/src/store/subjects/Subject/Subject.js index e1125da301..a7adceab1f 100644 --- a/packages/lib-classifier/src/store/subjects/Subject/Subject.js +++ b/packages/lib-classifier/src/store/subjects/Subject/Subject.js @@ -57,6 +57,10 @@ const Subject = types viewer = configuration.viewerType + // Volumetric Viewer is set at the Project level + if (!viewer && self.project?.isVolumetricViewer) + viewer = subjectViewers.volumetric + if (!viewer && counts.total === 1) { if (counts.images) { viewer = subjectViewers.singleImage diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js b/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js index fd7f3d27e0..bcb2fa37b9 100644 --- a/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js +++ b/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js @@ -11,14 +11,15 @@ const DEFAULT_HANDLER = () => {} export default function VolumetricViewer ({ loadingState = asyncStates.initialized, + onAnnotation = DEFAULT_HANDLER, onError = DEFAULT_HANDLER, onReady = DEFAULT_HANDLER, subject }) { const { data, loading, error } = useVolumetricSubject({ onError, onReady, subject }) - + const [modelState] = useState({ - annotations: ModelAnnotations(), + annotations: ModelAnnotations({ onAnnotation }), tool: ModelTool(), viewer: ModelViewer() }) @@ -46,14 +47,18 @@ export default function VolumetricViewer ({ /> } -export const VolumetricViewerData = ({ subjectData = '', subjectUrl = '' }) => { +export const VolumetricViewerData = ({ + onAnnotation = DEFAULT_HANDLER, + subjectData = '', + subjectUrl = '' +}) => { return { data: { config: {}, subjectData, subjectUrl, models: { - annotations: ModelAnnotations(), + annotations: ModelAnnotations({ onAnnotation }), tool: ModelTool(), viewer: ModelViewer() } diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelAnnotations.js b/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelAnnotations.js index 6737b0ea01..ea71aa1d6d 100644 --- a/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelAnnotations.js +++ b/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelAnnotations.js @@ -1,6 +1,6 @@ import { SortedSet, SortedSetUnion } from './../helpers/SortedSet.js' -const THRESHOLD_DEFAULT = 30 +const THRESHOLD_DEFAULT = 15 let ANNOTATION_COUNT = 0 // Creates the base object for an Annotation @@ -49,7 +49,7 @@ const History = { } } -export const ModelAnnotations = () => { +export const ModelAnnotations = ({ onAnnotation }) => { const annotationModel = { annotations: [], config: { @@ -106,6 +106,11 @@ export const ModelAnnotations = () => { annotationIndex, annotations: annotationModel.annotations }) + + // Send to update handler + const annotationExport = JSON.parse(JSON.stringify(annotationModel.annotations)) + annotationExport.forEach(a => a.points.all = a.points.all.data) + onAnnotation(annotationExport) }, active: ({ index }) => { // Update the Annotation Data @@ -206,6 +211,11 @@ export const ModelAnnotations = () => { annotationIndex: _index, annotations: annotationModel.annotations }) + + // Send to update handler + const annotationExport = JSON.parse(JSON.stringify(annotationModel.annotations)) + annotationExport.forEach(a => a.points.all = a.points.all.data) + onAnnotation(annotationExport) } } } diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/tests/ModelAnnotations.spec.js b/packages/lib-subject-viewers/src/VolumetricViewer/tests/ModelAnnotations.spec.js index 6fa8f8100a..f621b2df96 100644 --- a/packages/lib-subject-viewers/src/VolumetricViewer/tests/ModelAnnotations.spec.js +++ b/packages/lib-subject-viewers/src/VolumetricViewer/tests/ModelAnnotations.spec.js @@ -1,7 +1,8 @@ import { ModelAnnotations } from './../models/ModelAnnotations' describe('Component > VolumetricViewer > ModelAnnotations', () => { - const model = ModelAnnotations() + const ANNOTATION_THRESHOLD = 15 + const model = ModelAnnotations({ onAnnotation: () => {} }) const viewerMock = { setPointsAnnotationIndex: () => {} } @@ -44,7 +45,7 @@ describe('Component > VolumetricViewer > ModelAnnotations', () => { expect(obj.annotations[0]).to.equal(obj.annotation) expect(obj.annotation.label).to.equal('Annotation 2') - expect(obj.annotation.threshold).to.equal(30) + expect(obj.annotation.threshold).to.equal(ANNOTATION_THRESHOLD) expect(obj.annotation.points.active).deep.to.equal([activePoint]) expect(obj.annotation.points.connected).deep.to.equal([[]]) expect(obj.annotation.points.all.data).deep.to.equal([]) @@ -73,7 +74,7 @@ describe('Component > VolumetricViewer > ModelAnnotations', () => { expect(obj.annotations[1]).to.equal(obj.annotation) expect(obj.annotation.label).to.equal('Annotation 3') - expect(obj.annotation.threshold).to.equal(30) + expect(obj.annotation.threshold).to.equal(ANNOTATION_THRESHOLD) expect(obj.annotation.points.active).deep.to.equal([pointToAdd]) expect(obj.annotation.points.connected).deep.to.equal([[]]) expect(obj.annotation.points.all.data).deep.to.equal([]) @@ -139,7 +140,7 @@ describe('Component > VolumetricViewer > ModelAnnotations', () => { expect(obj.annotations[1]).to.equal(obj.annotation) expect(obj.annotation.label).to.equal('Annotation 4') - expect(obj.annotation.threshold).to.equal(30) + expect(obj.annotation.threshold).to.equal(ANNOTATION_THRESHOLD) expect(obj.annotation.points.active).deep.to.equal([activePoint]) expect(obj.annotation.points.connected).deep.to.equal([[]]) expect(obj.annotation.points.all.data).deep.to.equal([]) @@ -189,7 +190,7 @@ describe('Component > VolumetricViewer > ModelAnnotations', () => { active: [2], connected: [[]] }, - threshold: 30 + threshold: 15 }, { label: 'Annotation 4', @@ -197,7 +198,7 @@ describe('Component > VolumetricViewer > ModelAnnotations', () => { active: [3, 4], connected: [[], []] }, - threshold: 30 + threshold: 15 } ]) })