Skip to content

Commit

Permalink
VV - Implement Classifier Tool/Task (#6511)
Browse files Browse the repository at this point in the history
* Create Volumetric Task and stub out Annotation
  • Loading branch information
kieftrav authored Dec 10, 2024
1 parent 0a09c9d commit cd2796e
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ const StyledTaskInputLabelWrapper = styled.span`
`

const StyledText = styled(Text)`
display: block;
margin: 1em 0;
align-content: center;
padding-left: 15px;
padding-right: 15px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Volumetric Task

The volumetric task is a task type designed for use with structured volumetric JSON, as part of an NIH neuron mapping project. The task is a drawing/click-oriented task that constructs volumetric annotation data based on the clicked point in 3D space.

The volumetric task requires subjects with a location of application/json mime type. The subject's location content is used to initialize the structured subject data to be annotated.

The volumetric task does not support a required property.

The volumetric task does not support tags (i.e. insertion, deletion).

The volumetric task is disabled until the subject is loaded.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 PropTypes 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)`
margin: 0;
padding: 0;
width: 100%;
> *:first-child {
margin-top: 0;
}
`

const StyledToolIcon = styled.div`
background-color: #2d2d2d;
display: flex;
align-items: center;
padding-left: 15px;
&::after {
content: " ";
margin-right: 1ch;
white-space: pre;
}
> svg {
height: 1.5em;
vertical-align: bottom;
width: 1.5em;
}
`

function VolumetricTask({ disabled = false, task }) {
return (
<Box>
<StyledInstructionText as="legend" size="small">
<Markdownz>{task.instruction}</Markdownz>
</StyledInstructionText>

{/*
NOTE: Because there is only one active tool in a Volumetric project,
we do checked=true & index=0 to hardcode this tool as active
*/}

<TaskInput
checked={true}
disabled={disabled}
index={0}
label={"3D Viewer"}
labelIcon={
<StyledToolIcon>
<Blank viewBox="0 0 100 100">
<polygon
points={SVG_ARROW}
fill="blue"
transform="rotate(0 50 50)"
/>
<polygon
points={SVG_ARROW}
fill="green"
transform="rotate(135 50 50)"
/>
<polygon
points={SVG_ARROW}
fill="red"
transform="rotate(225 50 50)"
/>
</Blank>
</StyledToolIcon>
}
labelStatus={<InputStatus count={ANNOTATION_COUNT} />}
name="volumetric-tool"
type="radio"
/>
</Box>
)
}

VolumetricTask.propTypes = {
task: PropTypes.shape({
instruction: PropTypes.string,
}),
}

export default observer(VolumetricTask)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { composeStory } from "@storybook/react"
import { render, screen } from "@testing-library/react"
import Meta, { Default } from "./VolumetricTask.stories"

describe("VolumetricTask", function () {
const DefaultStory = composeStory(Default, Meta)

describe("when it renders", function () {
it("should show the instruction", function () {
render(<DefaultStory />)
expect(screen.getByText("Volumetric the task")).to.be.ok()
})

it("should show the label text", function () {
render(<DefaultStory />)
expect(screen.getByText("3D Viewer")).to.be.ok()
})

it("should show the classification count", function () {
render(<DefaultStory />)
expect(screen.getByText("InputStatus.drawn")).to.be.ok()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MockTask } from '@stories/components'
import VolumetricTask from './VolumetricTask'

export default {
title: 'Tasks / Volumetric',
component: VolumetricTask,
args: {},
argTypes: {}
}

const tasks = {
T0: {
strings: {
instruction: 'Volumetric the task',
},
taskKey: 'T0',
type: 'volumetric',
}
}

export function Default () {
return (<MockTask tasks={tasks} />)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { default as TaskComponent } from "./components/VolumetricTask"
import { default as TaskModel } from "./models/VolumetricTask"
import { default as AnnotationModel } from "./models/VolumetricAnnotation"

export default {
TaskComponent,
TaskModel,
AnnotationModel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { types } from 'mobx-state-tree'
import Annotation from '../../../models/Annotation'

const Volumetric = types
.model('Volumetric', {
taskType: types.literal('volumetric'),
value: types.optional(types.string, ''),
})

const VolumetricAnnotation = types.compose('VolumetricAnnotation', Annotation, Volumetric)
export default VolumetricAnnotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import cuid from "cuid"
import { types } from "mobx-state-tree"
import Task from "../../../models/Task"
import VolumetricAnnotation from "./VolumetricAnnotation"

const Volumetric = types
.model("Volumetric", {
annotation: types.safeReference(VolumetricAnnotation),
type: types.literal("volumetric"),
})
.views((self) => ({
defaultAnnotation(id = cuid()) {
return VolumetricAnnotation.create({
id,
task: self.taskKey,
taskType: self.type,
})
},
}))

const VolumetricTask = types.compose("VolumetricTask", Task, Volumetric)

export default VolumetricTask
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import VolumetricTask from "@plugins/tasks/experimental/volumetric"

describe("Model > VolumetricTask", function () {
const volumetricTask = {
taskKey: "T0",
type: "volumetric",
}

const singleChoiceTask = {
answers: [
{ label: "yes", next: "S2" },
{ label: "no", next: "S3" },
],
strings: {
question: "Do you exist?",
},
required: "",
taskKey: "T1",
type: "single",
}

it("should exist", function () {
const task = VolumetricTask.TaskModel.create(volumetricTask)
expect(task).to.be.ok()
expect(task).to.be.an("object")
})

it("should error for invalid tasks", function () {
let errorThrown = false
try {
VolumetricTask.TaskModel.create(singleChoiceTask)
} catch (e) {
errorThrown = true
}
expect(errorThrown).to.be.true()
})

describe("Views > defaultAnnotation", function () {
let task

before(function () {
task = VolumetricTask.TaskModel.create(volumetricTask)
})

it("should be a valid annotation", function () {
const annotation = task.defaultAnnotation()
expect(annotation.id).to.be.ok()
expect(annotation.task).to.equal("T0")
expect(annotation.taskType).to.equal("volumetric")
})

it("should generate unique annotations", function () {
const firstAnnotation = task.defaultAnnotation()
const secondAnnotation = task.defaultAnnotation()
expect(firstAnnotation.id).to.not.equal(secondAnnotation.id)
})
})

describe("with an annotation", function () {
let annotation
let task

before(function () {
task = VolumetricTask.TaskModel.create(volumetricTask)
annotation = task.defaultAnnotation()
})

it("should start up with an empty string", function () {
expect(annotation.value).to.equal("")
})

it("should update annotations", function () {
annotation.update("Hello there!")
expect(annotation.value).to.equal("Hello there!")
})
})
})
1 change: 1 addition & 0 deletions packages/lib-classifier/src/plugins/tasks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as text } from './text'
export { default as highlighter } from './experimental/highlighter'
export { default as textFromSubject } from './experimental/textFromSubject'
export { default as transcription } from './experimental/transcription'
export { default as volumetric } from './experimental/volumetric'
2 changes: 1 addition & 1 deletion packages/lib-classifier/src/store/Project/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Project = types
},

get isVolumetricViewer() {
return self.experimental_tools.includes('volumetricViewer')
return self.experimental_tools.includes('volumetricProject')
},
}))

Expand Down

0 comments on commit cd2796e

Please sign in to comment.