Skip to content

Commit

Permalink
VV - Implement Classifier Annotation Model (#6568)
Browse files Browse the repository at this point in the history
* Create Volumetric Annotation
  • Loading branch information
kieftrav authored Dec 19, 2024
1 parent 2150c33 commit c9ecdc9
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -32,7 +26,6 @@ function storeMapper(classifierStore) {

function SubjectViewer({
enableInteractionLayer,
isVolumetricViewer,
onError,
onSubjectReady,
subject,
Expand All @@ -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 fallback={<p>Suspense boundary</p>}>
<Viewer
enableInteractionLayer={enableInteractionLayer}
key={subject.id}
subject={subject}
loadingState={subjectReadyState}
onError={onError}
onReady={onSubjectReady}
viewerConfiguration={subject?.viewerConfiguration}
/>
</Suspense>
<Viewer
enableInteractionLayer={enableInteractionLayer}
key={subject.id}
loadingState={subjectReadyState}
onError={onError}
onReady={onSubjectReady}
subject={subject}
viewerConfiguration={subject?.viewerConfiguration}
/>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SubjectViewer
subjectQueueState={asyncStates.success}
subjectReadyState={asyncStates.success}
isVolumetricViewer={true}
subject={{
id: 'mock-id',
subjectJSON: 'mock-subject-json'
}}
/>)
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Volumetric Viewer

Implementation of the `lib-subject-viewers/VolumetricViewer` component.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const VolumetricSubjectMock = {
id: 'mockSubject',
locations: [
{
'application/json': 'https://zooniverse.org/subject.json'
},
],
subjectJSON: 'GRnI+hnIr5bIr5Z9r5Z9+uHIr5bIr5Z9r5Z9ZJZ9ZEv6r5Z9r5Z9ZJZ9ZEt9ZEsyGZZ9GRl9ZEt9ZEsyGUsyGQ=='
}
Original file line number Diff line number Diff line change
@@ -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(<VolumetricViewerWrapper
subjectQueueState={asyncStates.success}
subjectReadyState={asyncStates.success}
subject={{
id: 'mock-id',
subjectJSON: 'mock-subject-json'
}}
/>)
expect(screen.getByText('Suspense boundary')).to.exist()
expect(await screen.findByTestId('subject-viewer-volumetric')).to.exist()
})
})
Original file line number Diff line number Diff line change
@@ -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 (
<VolumetricViewerWrapper
loadingState={asyncStates.success}
subject={VolumetricSubjectMock}
/>
)
}
Original file line number Diff line number Diff line change
@@ -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 fallback={<p>Suspense boundary</p>}>
<VolumetricViewer
loadingState={loadingState}
onAnnotation={onAnnotationUpdate}
onError={onError}
onReady={onReady}
subject={subject}
/>
</Suspense>
}

export default VolumetricViewerWrapper
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,7 +21,8 @@ const viewers = {
singleText: SingleTextViewer,
singleVideo: SingleVideoViewer,
subjectGroup: SubjectGroupViewer,
variableStar: JSONDataViewer
variableStar: JSONDataViewer,
volumetric: VolumetricViewer
}

function getViewer (viewer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ describe('Helpers > subjectViewers', function () {
'singleText',
'singleVideo',
'subjectGroup',
'variableStar'
'variableStar',
'volumetric'
]

viewers.forEach(function (viewer) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)`
Expand All @@ -33,7 +31,11 @@ const StyledToolIcon = styled.div`
}
`

function VolumetricTask({ disabled = false, task }) {
function VolumetricTask({
annotation,
disabled = false,
task
}) {
return (
<Box>
<StyledInstructionText as="legend" size="small">
Expand Down Expand Up @@ -71,7 +73,7 @@ function VolumetricTask({ disabled = false, task }) {
</Blank>
</StyledToolIcon>
}
labelStatus={<InputStatus count={ANNOTATION_COUNT} />}
labelStatus={<InputStatus count={annotation.value.length} />}
name="volumetric-tool"
type="radio"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
})
})
})
4 changes: 4 additions & 0 deletions packages/lib-classifier/src/store/subjects/Subject/Subject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down Expand Up @@ -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()
}
Expand Down
Loading

0 comments on commit c9ecdc9

Please sign in to comment.