Skip to content

Commit

Permalink
Merge pull request #393 from lunit-io/feature/VIEWER-135-add-onChange
Browse files Browse the repository at this point in the history
VIEWER-135 / add onChange prop to AnnotationOverlay
  • Loading branch information
LTakhyunKim authored Apr 19, 2023
2 parents 7fa3ccb + 1db3710 commit 5d7e07c
Show file tree
Hide file tree
Showing 20 changed files with 414 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ describe(
// given
cy.get('[data-cy-initial-annotations]').click({ force: true })
cy.get('[value="line"]').click({ force: true })
cy.get('[data-cy-name="reset-button"]').click()
cy.get('[data-cy-id]').should('have.length', 0)

// when
Expand Down
56 changes: 33 additions & 23 deletions apps/insight-viewer-dev/containers/Annotation/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Box, Switch, Radio, RadioGroup, Stack, Button } from '@chakra-ui/react'
import { Resizable } from 're-resizable'
import InsightViewer, { useImage, LineHeadMode } from '@lunit/insight-viewer'
import { useViewport } from '@lunit/insight-viewer/viewport'
import { useAnnotation, AnnotationMode, AnnotationOverlay } from '@lunit/insight-viewer/annotation'
import { AnnotationMode, AnnotationOverlay } from '@lunit/insight-viewer/annotation'
import { INITIAL_POLYGON_ANNOTATIONS } from '@insight-viewer-library/fixtures'
import useImageSelect from '../../../components/ImageSelect/useImageSelect'

import type { Annotation } from '@lunit/insight-viewer/annotation'

const style = {
display: 'flex',
alignItems: 'center',
Expand All @@ -19,12 +21,16 @@ const DEFAULT_SIZE = { width: 700, height: 700 }
function AnnotationDrawerContainer(): JSX.Element {
const viewerRef = useRef<HTMLDivElement>(null)

const [hasInitialAnnotations, setHasInitialAnnotations] = useState<boolean>(true)
const [annotations, setAnnotations] = useState<Annotation[]>(hasInitialAnnotations ? INITIAL_POLYGON_ANNOTATIONS : [])
const [hoveredAnnotation, setHoveredAnnotation] = useState<Annotation | null>(null)
const [selectedAnnotation, setSelectedAnnotation] = useState<Annotation | null>(null)

const [annotationMode, setAnnotationMode] = useState<AnnotationMode>('polygon')
const [lineHeadMode, setLineHeadMode] = useState<LineHeadMode>('normal')
const [isDrawing, setIsDrawing] = useState<boolean>(true)
const [isEditing, setIsEditing] = useState<boolean>(false)
const [isShowLabel, setIsShowLabel] = useState<boolean>(false)
const [hasInitialAnnotations, setHasInitialAnnotations] = useState<boolean>(true)

const { ImageSelect, selected } = useImageSelect()
const { loadingState, image } = useImage({
Expand All @@ -34,19 +40,26 @@ function AnnotationDrawerContainer(): JSX.Element {
image,
element: viewerRef.current,
})
const {
annotations,
hoveredAnnotation,
selectedAnnotation,
addAnnotation,
removeAnnotation,
hoverAnnotation,
selectAnnotation,
removeAllAnnotation,
resetAnnotation,
} = useAnnotation({
initialAnnotation: hasInitialAnnotations ? INITIAL_POLYGON_ANNOTATIONS : undefined,
})

const handleAnnotationsChange = (annotations: Annotation[]) => {
setAnnotations(annotations)
}

const handleAnnotationSelect = (annotation: Annotation | null) => {
setSelectedAnnotation(annotation)
}

const handleAnnotationHover = (annotation: Annotation | null) => {
setHoveredAnnotation(annotation)
}

const handleRemoveAllAnnotation = () => {
setAnnotations([])
}

const handleResetAnnotation = () => {
setAnnotations(hasInitialAnnotations ? INITIAL_POLYGON_ANNOTATIONS : [])
}

const handleInitialAnnotationChange = (event: ChangeEvent<HTMLInputElement>) => {
setHasInitialAnnotations(event.target.checked)
Expand Down Expand Up @@ -130,10 +143,10 @@ function AnnotationDrawerContainer(): JSX.Element {
<Radio value="arrow">Arrow</Radio>
</Stack>
</RadioGroup>
<Button data-cy-name="reset-button" size="sm" mb={2} mr={3} colorScheme="blue" onClick={resetAnnotation}>
<Button data-cy-name="reset-button" size="sm" mb={2} mr={3} colorScheme="blue" onClick={handleResetAnnotation}>
reset
</Button>
<Button data-cy-name="remove-button" size="sm" mb={2} colorScheme="blue" onClick={removeAllAnnotation}>
<Button data-cy-name="remove-button" size="sm" mb={2} colorScheme="blue" onClick={handleRemoveAllAnnotation}>
remove all
</Button>
<Resizable style={style} defaultSize={DEFAULT_SIZE} className={`annotation ${annotationMode}`}>
Expand All @@ -142,17 +155,14 @@ function AnnotationDrawerContainer(): JSX.Element {
<AnnotationOverlay
isDrawing={isDrawing}
clickAction={isEditing ? 'select' : 'remove'}
width={700}
height={700}
mode={annotationMode}
annotations={annotations}
hoveredAnnotation={hoveredAnnotation}
selectedAnnotation={selectedAnnotation}
showAnnotationLabel={isShowLabel}
onAdd={addAnnotation}
onMouseOver={hoverAnnotation}
onRemove={removeAnnotation}
onSelect={selectAnnotation}
onHover={handleAnnotationHover}
onSelect={handleAnnotationSelect}
onChange={handleAnnotationsChange}
/>
)}
</InsightViewer>
Expand Down
54 changes: 32 additions & 22 deletions apps/insight-viewer-dev/containers/Measurement/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import React, { useRef, useState, ChangeEvent, useEffect } from 'react'
import { Box, Switch, Radio, RadioGroup, Stack, Button } from '@chakra-ui/react'
import { Resizable } from 're-resizable'
import InsightViewer, { useImage } from '@lunit/insight-viewer'
import { AnnotationOverlay, useAnnotation, AnnotationMode } from '@lunit/insight-viewer/annotation'
import { AnnotationOverlay } from '@lunit/insight-viewer/annotation'
import { useViewport } from '@lunit/insight-viewer/viewport'
import useImageSelect from '../../../components/ImageSelect/useImageSelect'

import type { Annotation, AnnotationMode } from '@lunit/insight-viewer/annotation'

const style = {
display: 'flex',
alignItems: 'center',
Expand All @@ -19,7 +21,11 @@ const DEFAULT_SIZE = { width: 700, height: 700 }
function MeasurementDrawerContainer(): JSX.Element {
const viewerRef = useRef<HTMLDivElement>(null)

const [measurementMode, setMeasurementMode] = useState<AnnotationMode>('ruler')
const [annotations, setAnnotations] = useState<Annotation[]>([])
const [hoveredAnnotation, setHoveredAnnotation] = useState<Annotation | null>(null)
const [selectedAnnotation, setSelectedAnnotation] = useState<Annotation | null>(null)

const [annotationMode, setAnnotationMode] = useState<AnnotationMode>('ruler')
const [isEditing, setIsEditing] = useState(false)
const [isDrawing, setIsDrawing] = useState(true)

Expand All @@ -32,19 +38,25 @@ function MeasurementDrawerContainer(): JSX.Element {
image,
element: viewerRef.current,
})
const {
annotations,
hoveredAnnotation,
selectedAnnotation,
addAnnotation,
hoverAnnotation,
removeAnnotation,
selectAnnotation,
removeAllAnnotation,
} = useAnnotation()

const handleAnnotationsChange = (annotations: Annotation[]) => {
setAnnotations(annotations)
}

const handleAnnotationSelect = (annotation: Annotation | null) => {
setSelectedAnnotation(annotation)
}

const handleAnnotationHover = (annotation: Annotation | null) => {
setHoveredAnnotation(annotation)
}

const handleRemoveAllAnnotation = () => {
setAnnotations([])
}

const handleMeasurementModeClick = (mode: AnnotationMode) => {
setMeasurementMode(mode)
setAnnotationMode(mode)
}

const handleEditModeChange = (event: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -82,17 +94,17 @@ function MeasurementDrawerContainer(): JSX.Element {
<Box>
Edit enabled (E) <Switch data-cy-edit={isEditing} onChange={handleEditModeChange} isChecked={isEditing} />
</Box>
<RadioGroup onChange={handleMeasurementModeClick} value={measurementMode}>
<RadioGroup onChange={handleMeasurementModeClick} value={annotationMode}>
<Stack direction="row">
<Radio value="ruler">Ruler</Radio>
<Radio value="area">Area</Radio>
</Stack>
</RadioGroup>
<Button data-cy-name="remove-button" marginBottom="10px" colorScheme="blue" onClick={removeAllAnnotation}>
<Button data-cy-name="remove-button" marginBottom="10px" colorScheme="blue" onClick={handleRemoveAllAnnotation}>
remove all
</Button>

<Box data-cy-loaded={loadingState} className={`measurement ${measurementMode}`}>
<Box data-cy-loaded={loadingState} className={`measurement ${annotationMode}`}>
<Resizable style={style} defaultSize={DEFAULT_SIZE}>
<InsightViewer viewerRef={viewerRef} image={image} viewport={viewport} onViewportChange={setViewport}>
{loadingState === 'success' && (
Expand All @@ -102,14 +114,12 @@ function MeasurementDrawerContainer(): JSX.Element {
annotations={annotations}
selectedAnnotation={selectedAnnotation}
hoveredAnnotation={hoveredAnnotation}
onAdd={addAnnotation}
onMouseOver={hoverAnnotation}
onSelect={selectAnnotation}
onRemove={removeAnnotation}
onHover={handleAnnotationHover}
onSelect={handleAnnotationSelect}
clickAction={isEditing ? 'select' : 'remove'}
isDrawing={isDrawing}
mode={measurementMode}
// If no mode is defined, the default value is ruler.
mode={annotationMode}
onChange={handleAnnotationsChange}
/>
)}
</InsightViewer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export interface AnnotationOverlayProps {
selectedAnnotation?: Annotation | null
annotations: Annotation[]

onAdd?: (annotation: Annotation) => void
onClick?: (annotation: Annotation) => void
onRemove?: (annotation: Annotation) => void
onChange?: (annotations: Annotation[]) => void
onSelect?: (annotation: Annotation | null) => void
onMouseOver?: (annotation: Annotation | null) => void
onHover?: (annotation: Annotation | null) => void
onAdd?: (annotation: Annotation) => Annotation | null
onRemove?: (annotation: Annotation) => Annotation | null
elementAttrs?: (annotation: Annotation, showOutline: boolean) => SVGProps<SVGPolygonElement>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import React from 'react'
import { AnnotationDrawer } from '../AnnotationDrawer'
import { AnnotationsViewer } from '../AnnotationsViewer'

import { validateAnnotation } from './utils/validateAnnotation'

import type { Annotation } from '../../types'
import type { AnnotationOverlayProps } from './AnnotationOverlay.types'

export const AnnotationOverlay = ({
Expand All @@ -19,10 +22,52 @@ export const AnnotationOverlay = ({
selectedAnnotation,
showAnnotationLabel,
onAdd,
onMouseOver,
onHover,
onRemove,
onSelect,
onChange,
}: AnnotationOverlayProps): JSX.Element => {
const handleAddAnnotation = (annotation: Annotation) => {
let addedTargetAnnotation: Annotation | null = annotation

addedTargetAnnotation = validateAnnotation(addedTargetAnnotation)

if (addedTargetAnnotation && onAdd) {
addedTargetAnnotation = onAdd(addedTargetAnnotation)
}

if (!onChange) return

if (!addedTargetAnnotation) {
onChange(annotations)
return
}

if (selectedAnnotation) {
onChange(annotations.map((prevAnnotation) => (prevAnnotation.id === annotation.id ? annotation : prevAnnotation)))
return
}

onChange([...annotations, annotation])
}

const handleRemoveAnnotation = (annotation: Annotation) => {
let removedTargetAnnotation: Annotation | null = annotation

if (onRemove) {
removedTargetAnnotation = onRemove(annotation)
}

if (!onChange) return

if (!removedTargetAnnotation) {
onChange(annotations)
return
}

onChange(annotations.filter((a) => a.id !== annotation.id))
}

return (
<>
<AnnotationsViewer
Expand All @@ -35,8 +80,8 @@ export const AnnotationOverlay = ({
style={style}
showOutline={showOutline}
showElementLabel={showAnnotationLabel}
onMouseOver={isDrawing ? onMouseOver : undefined}
onClick={isDrawing ? (clickAction === 'select' ? onSelect : onRemove) : undefined}
onHover={isDrawing ? onHover : undefined}
onClick={isDrawing ? (clickAction === 'select' ? onSelect : handleRemoveAnnotation) : undefined}
/>
<AnnotationDrawer
width={width}
Expand All @@ -50,7 +95,7 @@ export const AnnotationOverlay = ({
isDrawing={isDrawing}
clickAction={clickAction}
mode={mode}
onAdd={onAdd}
onAdd={handleAddAnnotation}
onSelect={onSelect}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getIsComplexPolygon } from './getIsComplexPolygon'
import { ID1_POLYGON, ID2_POLYGON, COMPLEX_POLYGON } from '../../../../mocks/polygons'

describe('getIsComplexPolygon:', () => {
it('the id 1 contour polygon shouldn`t be complex', () => {
expect(getIsComplexPolygon(ID1_POLYGON.points)).toBeFalsy()
})
it('the id 2 contour polygon shouldn`t be complex', () => {
expect(getIsComplexPolygon(ID2_POLYGON.points)).toBeFalsy()
})
it('the polygon should be complex', () => {
expect(getIsComplexPolygon(COMPLEX_POLYGON.points)).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable no-plusplus */

import { getIsIntersection } from './getIsIntersection'
import { Point } from '../../../types'

/**
* Check if polygon is a complex polygon with intersection
* @param polygon Array<[number, number]>
*/
export function getIsComplexPolygon(polygon: Point[]): boolean {
// Adds an initial point as an endpoint to create a closed polygon
const closedPolygon: Point[] = [...polygon, polygon[0]]
const max = closedPolygon.length

let i = -1
while (++i < max - 2) {
// Since there is no need to search before the current point i,
// it searches from the point after i
let n = i + 2
while (++n < max - 1) {
// Check if line a -> b and line c -> d intersect
// i becomes point a and i + 1 becomes point b
// i + 2 becomes c, i + 3 becomes d
// i + 1 -> i + 2 is in contact with i -> i + 1 and cannot intersect, so we exclude it
if (getIsIntersection(closedPolygon[i], closedPolygon[i + 1], closedPolygon[n], closedPolygon[n + 1])) {
return true
}
}
}

return false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Point } from '../../../../types'
import { getIsIntersection } from './getIsIntersection'

describe('getIsIntersection:', () => {
/*
* a is line 1 start point
* b is line 1 end point
* c is line 2 start point
* d is line 2 end point
*
* ab is line 1, cd is line 2
*/

it('ab and cd should be intersect', () => {
const [a, b, c, d]: Point[] = [
[0, 0],
[10, 10],
[10, 0],
[0, 10],
]

expect(getIsIntersection(a, b, c, d)).toBeTruthy()
})
it('ab and cd shouldn`t be intersect', () => {
const [a, b, c, d]: Point[] = [
[0, 0],
[10, 0],
[0, 10],
[10, 10],
]

expect(getIsIntersection(a, b, c, d)).toBeFalsy()
})
})
Loading

0 comments on commit 5d7e07c

Please sign in to comment.