Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix previous marks from caesar render for transcription task #2001

Merged
merged 5 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import SHOWN_MARKS from '@helpers/shownMarks'

function storeMapper (stores) {
const { active: subject, isThereMetadata } = stores.classifierStore.subjects
const { activeStepTasks } = stores.classifierStore.workflowSteps
const [activeInteractionTask] = activeStepTasks.filter(task => task.type === 'drawing' || task.type === 'transcription') || {}
const { interactionTask } = stores.classifierStore.workflowSteps
const upp = stores.classifierStore.userProjectPreferences.active
return {
activeInteractionTask,
interactionTask,
isThereMetadata,
subject,
upp
Expand All @@ -37,8 +36,15 @@ class MetaTools extends React.Component {

// TODO: Add fallbacks for when Panoptes is not serializing the subject favorite info
render () {
const { activeInteractionTask, className, isThereMetadata, screenSize, subject, upp } = this.props
const { shownMarks, marks, togglePreviousMarks, type } = activeInteractionTask || {}
const {
className,
interactionTask,
isThereMetadata,
screenSize,
subject,
upp
} = this.props
const { shownMarks, marks, togglePreviousMarks, type } = interactionTask
const gap = (screenSize === 'small') ? 'xsmall' : 'small'
const margin = (screenSize === 'small') ? { top: 'small' } : 'none'

Expand All @@ -60,9 +66,9 @@ class MetaTools extends React.Component {
disabled={!subject || !upp}
onClick={this.addToCollection}
/>
{activeInteractionTask && (
{Object.keys(interactionTask).length > 0 && (
<HidePreviousMarksButton
disabled={marks.length === 0}
disabled={marks?.length === 0}
shownMarks={shownMarks}
type={type}
onClick={(state) => togglePreviousMarks(state)}
Expand All @@ -73,25 +79,21 @@ class MetaTools extends React.Component {
}
}

MetaTools.defaultProps = {
activeInteractionTask: {
shownMarks: SHOWN_MARKS.ALL,
togglePreviousMarks: () => {},
type: ''
},
MetaTools.wrappedComponent.defaultProps = {
className: '',
interactionTask: {},
isThereMetadata: false,
subject: null,
upp: null
}

MetaTools.propTypes = {
activeInteractionTask: PropTypes.shape({
MetaTools.wrappedComponent.propTypes = {
className: PropTypes.string,
interactionTask: PropTypes.shape({
shownMarks: PropTypes.string,
togglePreviousMarks: PropTypes.func,
type: PropTypes.string
}),
className: PropTypes.string,
isThereMetadata: PropTypes.bool,
screenSize: PropTypes.string,
subject: PropTypes.object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import Metadata from './components/Metadata'
import CollectionsButton from './components/CollectionsButton'
import HidePreviousMarksButton from './components/HidePreviousMarksButton'
import { Factory } from 'rosie'
import SHOWN_MARKS from '@helpers/shownMarks'

const subjectWithMetadata = Factory.build('subject', { metadata: { foo: 'bar' } })

const favoriteSubject = Factory.build('subject', { favorite: true })

const spy = sinon.spy()

const activeInteractionTask = {
const interactionTask = {
hidePreviousMarks: false,
marks: [{ x: 0, y: 0 }],
shownMarks: SHOWN_MARKS.ALL,
togglePreviousMarks: spy
}

Expand Down Expand Up @@ -112,7 +114,7 @@ describe('Component > MetaTools', function () {
let wrapper

beforeEach(function () {
wrapper = shallow(<MetaTools.wrappedComponent activeInteractionTask={activeInteractionTask} />)
wrapper = shallow(<MetaTools.wrappedComponent interactionTask={interactionTask} />)
})

it('should render', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import React, { Component } from 'react'

import InteractionLayer from './InteractionLayer'
import DrawingToolMarks from './components/DrawingToolMarks'
import PreviousMarks from './components/PreviousMarks'
import SHOWN_MARKS from '@helpers/shownMarks'

function storeMapper (stores) {
Expand Down Expand Up @@ -61,16 +61,6 @@ class InteractionLayerContainer extends Component {

return (
<>
{shownMarks === SHOWN_MARKS.ALL && interactionTaskAnnotations.map((annotation) => {
const annotationValuesPerFrame = annotation.value.filter(value => value.frame === frame)
return (
<DrawingToolMarks
key={annotation.task}
marks={annotationValuesPerFrame}
scale={scale}
/>
)
})}
{activeInteractionTask && activeTool &&
<InteractionLayer
activeMark={activeMark}
Expand All @@ -87,6 +77,7 @@ class InteractionLayerContainer extends Component {
width={width}
/>
}
<PreviousMarks scale={scale} />
srallen marked this conversation as resolved.
Show resolved Hide resolved
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { shallow } from 'enzyme'
import React from 'react'
import InteractionLayerContainer from './InteractionLayerContainer'
import InteractionLayer from './InteractionLayer'
import DrawingToolMarks from './components/DrawingToolMarks'
import PreviousMarks from './components/PreviousMarks'
import SHOWN_MARKS from '@helpers/shownMarks'

describe('Component > InteractionLayerContainer', function () {
Expand Down Expand Up @@ -41,6 +41,17 @@ describe('Component > InteractionLayerContainer', function () {
expect(wrapper).to.be.ok()
})

it('should render PreviousMarks', function () {
const wrapper = shallow(
<InteractionLayerContainer.wrappedComponent
activeInteractionTask={drawingTask}
height={height}
width={width}
/>
)
expect(wrapper.find(PreviousMarks)).to.have.lengthOf(1)
})

describe('with an active drawing task and drawing tool', function () {
let wrapper

Expand All @@ -59,34 +70,6 @@ describe('Component > InteractionLayerContainer', function () {
})
})

describe('with annotations from previous drawing or transcription tasks', function () {
it('should render DrawingToolMarks', function () {
const wrapper = shallow(
<InteractionLayerContainer.wrappedComponent
interactionTaskAnnotations={drawingAnnotations}
frame={0}
height={height}
width={width}
/>
)
expect(wrapper.find(DrawingToolMarks)).to.have.lengthOf(2)
})

it('should render DrawingToolMarks with marks per frame', function () {
const wrapper = shallow(
<InteractionLayerContainer.wrappedComponent
interactionTaskAnnotations={drawingAnnotations}
frame={0}
height={height}
width={width}
/>
)

expect(wrapper.find(DrawingToolMarks).first().prop('marks')).to.have.lengthOf(1)
expect(wrapper.find(DrawingToolMarks).last().prop('marks')).to.have.lengthOf(2)
})
})

describe('with transcription task', function () {
let wrapper
const hidingTask = {
Expand Down Expand Up @@ -189,10 +172,6 @@ describe('Component > InteractionLayerContainer', function () {
)
})

it('should hide previous annotations', function () {
expect(wrapper.find(DrawingToolMarks)).to.have.lengthOf(0)
})

it('should hide all marks before the hiding index', function () {
const { marks } = wrapper.find(InteractionLayer).props()
expect(marks).to.have.lengthOf(1)
Expand All @@ -215,7 +194,6 @@ describe('Component > InteractionLayerContainer', function () {

const marks = wrapper.find(InteractionLayer).props().marks
expect(marks).to.have.lengthOf(2)
expect(wrapper.find(DrawingToolMarks)).to.have.lengthOf(0)
})
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import PropTypes from 'prop-types'
import DrawingToolMarks from '../DrawingToolMarks'
import SHOWN_MARKS from '@helpers/shownMarks'

function PreviousMarks (props) {
const {
frame = 0,
interactionTaskAnnotations = [],
scale = 1,
shownMarks = 'ALL'
} = props
const marksToShow = shownMarks === SHOWN_MARKS.ALL || shownMarks === SHOWN_MARKS.USER

if (interactionTaskAnnotations?.length > 0 && marksToShow) {
// Wrapping the array in an react fragment because enzyme errors otherwise
// React v16 allows this, though.
return (
<>
{interactionTaskAnnotations.map((annotation) => {
const annotationValuesPerFrame = annotation.value.filter(value => value.frame === frame)
Comment on lines +20 to +21
Copy link
Contributor

@goplayoutside3 goplayoutside3 Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eatyourgreens this is a long-ago PR, but I have a question particularly about this line introduced here, which is still used in production. annotationValuesPerFrame is intended to filter marks based on the frame passed to PreviousMarks component. However, Travis and I have found that's not always the case and are unsure how long the following bug has been around ~

On projects with multi-frame subjects and more than one workflow step, previous annotations do not filter correctly when switching between frames. For instance, How Did We Get Here is a transcription project with drawing as the first step and a yes/no as the second step. If I draw marks on all the frames in the first step, then go to the 2nd step, navigating between frames renders marks on only one frame. I think the expected behavior is that previous marks should always render on their respective frames. Any thoughts on how to fix this filter?

(More dicussion here).

Copy link
Contributor

@eatyourgreens eatyourgreens Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm the bug. Here, I've added two lines to frame 0 and one line to frame 1. classification.annotations only contains one line ID for task T0, not three.

Are annotations being lost on live projects that transcribe more than one page per subject?

Screenshot of the dev classifier for How Did We Get Here. The console is open to show that classification.annotations only contains one transcribed line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I go back to the drawing step, my drawn lines are there. They haven't been added to the classification. So my thought would be that the bug is in the code that adds mark IDs to the current classification.

Screenshot of the dev classifier for How Did We Get Here. There are two drawn lines on the page, but neither are stored in classification.annotations yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I then add a third mark to frame 0, classification.annotations updates to show the three lines from that frame, but loses the line from frame 1.

Screenshot of the dev classifier for How Did We Get Here. I have added a third line to the first frame of the active subject, and `classification.annotations` now shows the three mark IDs for those lines, but has lost the ID of the fourth line (on the second frame.)

Copy link
Contributor

@eatyourgreens eatyourgreens Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on that, I would say that previousAnnotations is working as expected, but the classification isn't being updated correctly when you draw on more than one frame of a subject. classification.annotations only contains mark IDs for the active frame. I'd expect it to contain the mark IDs for all the transcription lines that I've made.

const markIDs = marks.map((mark) => mark.id)
annotation.update([...markIDs, mark.id])

Since this bug is in production, and there are projects with subjects that have more than one page, I would definitely double check that classifications are being recorded properly when a volunteer adds lines to more than one page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've suggested a fix for the markIDs array in #5919.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the bug, by the way. marks here is filtered by frame before being added to the classification in InteractionLayer.

Copy link
Contributor

@eatyourgreens eatyourgreens Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#5923 is a more robust fix, replacing the manual update with an autorun reaction that runs whenever the task’s marks change.

return (
<DrawingToolMarks
key={annotation.task}
marks={annotationValuesPerFrame}
scale={scale}
/>
)
})}
</>
)
}

return null
}

export default PreviousMarks
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { shallow } from 'enzyme'
import React from 'react'
import PreviousMarks from './PreviousMarks'
import DrawingToolMarks from '../DrawingToolMarks'
import SHOWN_MARKS from '@helpers/shownMarks'

describe('Component > PreviousMarks', function () {
const interactionTaskAnnotations = [{
task: 'T2',
value: [
{ id: 'point1', frame: 0, toolIndex: 0, x: 100, y: 200 }
]
}, {
task: 'T3',
value: [
{ id: 'line1', frame: 0, toolIndex: 0, x1: 100, y1: 200, x2: 150, y2: 200 },
{ id: 'line2', frame: 0, toolIndex: 0, x1: 200, y1: 300, x2: 250, y2: 300 },
{ id: 'line3', frame: 1, toolIndex: 0, x1: 150, y1: 250, x2: 100, y2: 250 },
{ id: 'line4', frame: 1, toolIndex: 0, x1: 250, y1: 350, x2: 200, y2: 350 }
]
}]

it('should render without crashing', function () {
const wrapper = shallow(<PreviousMarks />)
expect(wrapper).to.be.ok()
})

it('should pass scale along as a prop to each DrawingToolMarks component', function () {
const wrapper = shallow(<PreviousMarks interactionTaskAnnotations={interactionTaskAnnotations} scale={2} />)
expect(wrapper.find(DrawingToolMarks).first().props().scale).to.equal(2)
expect(wrapper.find(DrawingToolMarks).last().props().scale).to.equal(2)
})

describe('when there are no interaction task annotations', function () {
it('should render null', function () {
const wrapper = shallow(<PreviousMarks />)
expect(wrapper.html()).to.be.null()
})
})

describe('when there are interaction task annotations', function () {
it('should render a DrawingToolMarks for each task', function () {
const wrapper = shallow(<PreviousMarks interactionTaskAnnotations={interactionTaskAnnotations} />)
expect(wrapper.find(DrawingToolMarks)).to.have.lengthOf(2)
expect(wrapper.find(DrawingToolMarks).first().key()).to.equal(interactionTaskAnnotations[0].task)
expect(wrapper.find(DrawingToolMarks).last().key()).to.equal(interactionTaskAnnotations[1].task)
})

it('should pass along the correct number of marks per task by frame', function () {
const firstTaskAnnotationsFirstFrameFirstMark = interactionTaskAnnotations[0].value[0]
const secondTaskAnnotationsFirstFrameFirstMark = interactionTaskAnnotations[1].value[0]
const secondTaskAnnotationsFirstFrameSecondMark = interactionTaskAnnotations[1].value[1]
const secondTaskAnnotationsSecondFrameFirstMark = interactionTaskAnnotations[1].value[2]
const secondTaskAnnotationsSecondFrameSecondMark = interactionTaskAnnotations[1].value[3]
const wrapper = shallow(<PreviousMarks interactionTaskAnnotations={interactionTaskAnnotations} />)
let firstTaskAnnotationRenderedMarks = wrapper.find(DrawingToolMarks).first().props().marks
let secondTaskAnnotationRenderedMarks = wrapper.find(DrawingToolMarks).last().props().marks
expect(firstTaskAnnotationRenderedMarks).to.have.lengthOf(1)
expect(firstTaskAnnotationRenderedMarks[0]).to.deep.equal(firstTaskAnnotationsFirstFrameFirstMark)
expect(secondTaskAnnotationRenderedMarks).to.have.lengthOf(2)
expect(secondTaskAnnotationRenderedMarks[0]).to.deep.equal(secondTaskAnnotationsFirstFrameFirstMark)
expect(secondTaskAnnotationRenderedMarks[1]).to.deep.equal(secondTaskAnnotationsFirstFrameSecondMark)
wrapper.setProps({ frame: 1 })
firstTaskAnnotationRenderedMarks = wrapper.find(DrawingToolMarks).first().props().marks
secondTaskAnnotationRenderedMarks = wrapper.find(DrawingToolMarks).last().props().marks
expect(firstTaskAnnotationRenderedMarks).to.have.lengthOf(0)
expect(secondTaskAnnotationRenderedMarks).to.have.lengthOf(2)
expect(secondTaskAnnotationRenderedMarks[0]).to.deep.equal(secondTaskAnnotationsSecondFrameFirstMark)
expect(secondTaskAnnotationRenderedMarks[1]).to.deep.equal(secondTaskAnnotationsSecondFrameSecondMark)
})
})

describe('when shown marks is USER', function () {
it('should not render DrawingToolMarks if there are not annotations', function () {
const wrapper = shallow(<PreviousMarks shownMarks={SHOWN_MARKS.USER} />)
expect(wrapper.html()).to.be.null()
})

it('should render DrawingToolMarks if there are also annotations', function () {
const wrapper = shallow(<PreviousMarks interactionTaskAnnotations={interactionTaskAnnotations} shownMarks={SHOWN_MARKS.USER} />)
expect(wrapper.find(DrawingToolMarks)).to.have.lengthOf(2)
})
})

describe('when shown marks is NONE', function () {
it('should render null', function () {
const wrapper = shallow(<PreviousMarks shownMarks={SHOWN_MARKS.NONE} />)
expect(wrapper.html()).to.be.null()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import PropTypes from 'prop-types'
import { MobXProviderContext, observer } from 'mobx-react'
import PreviousMarks from './PreviousMarks'

function useStores() {
const stores = React.useContext(MobXProviderContext)
const { interactionTask } = stores.classifierStore.workflowSteps
const {
active: classification
} = stores.classifierStore.classifications
const interactionTaskAnnotations = classification?.interactionTaskAnnotations || []
const {
frame
} = stores.classifierStore.subjectViewer
return { frame, interactionTask, interactionTaskAnnotations }
}

function PreviousMarksConnector({ scale, ...rest }) {
const {
frame = 0,
interactionTask,
interactionTaskAnnotations
} = useStores()
const { shownMarks } = interactionTask
return (
<PreviousMarks
frame={frame}
interactionTaskAnnotations={interactionTaskAnnotations}
scale={scale}
shownMarks={shownMarks}
{...rest}
/>
)
}

PreviousMarksConnector.propTypes = {
scale: PropTypes.number
}

export default observer(PreviousMarksConnector)
Loading
Loading