diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b40eabe9f2..c646e03f8f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.52.1", "react-intl": "^6.6.2", + "react-konva": "^18.2.10", "react-paginate": "^8.2.0", "react-router-bootstrap": "^0.26.2", "react-router-dom": "^6.22.0", @@ -4921,6 +4922,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", @@ -11369,6 +11378,17 @@ "set-function-name": "^2.0.1" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -13606,6 +13626,26 @@ "node": ">= 8" } }, + "node_modules/konva": { + "version": "9.3.18", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.18.tgz", + "integrity": "sha512-ad5h0Y9phUrinBrKXyIISbURRHQO7Rx5cz7mAEEfdVCs45gDqRD8Y0I0nJRk8S6iqEbiRE87CEZu5GVSnU8oow==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "peer": true + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -16865,6 +16905,7 @@ "version": "10.6.2", "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -17401,6 +17442,36 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-konva": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", + "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "dependencies": { + "@types/react-reconciler": "^0.28.2", + "its-fine": "^1.1.1", + "react-reconciler": "~0.29.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -17443,6 +17514,21 @@ } } }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -24722,6 +24808,12 @@ "@types/react": "*" } }, + "@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "requires": {} + }, "@types/react-transition-group": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", @@ -29336,6 +29428,14 @@ "set-function-name": "^2.0.1" } }, + "its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "requires": { + "@types/react-reconciler": "^0.28.0" + } + }, "jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -30973,6 +31073,12 @@ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==" }, + "konva": { + "version": "9.3.18", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.18.tgz", + "integrity": "sha512-ad5h0Y9phUrinBrKXyIISbURRHQO7Rx5cz7mAEEfdVCs45gDqRD8Y0I0nJRk8S6iqEbiRE87CEZu5GVSnU8oow==", + "peer": true + }, "language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -33437,6 +33543,17 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-konva": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", + "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "requires": { + "@types/react-reconciler": "^0.28.2", + "its-fine": "^1.1.1", + "react-reconciler": "~0.29.0", + "scheduler": "^0.23.0" + } + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -33461,6 +33578,15 @@ "match-sorter": "^6.0.2" } }, + "react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3befd3ea34..348432e882 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.52.1", "react-intl": "^6.6.2", + "react-konva": "^18.2.10", "react-paginate": "^8.2.0", "react-router-bootstrap": "^0.26.2", "react-router-dom": "^6.22.0", @@ -100,10 +101,13 @@ "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" - }, - "jest":{ + }, + "jest": { "collectCoverage": true, - "coverageReporters": ["lcov", "text"], + "coverageReporters": [ + "lcov", + "text" + ], "coverageDirectory": "./coverage" } } diff --git a/frontend/src/AuthenticatedSwitch.jsx b/frontend/src/AuthenticatedSwitch.jsx index bcdff0f5fc..d62107b5cb 100644 --- a/frontend/src/AuthenticatedSwitch.jsx +++ b/frontend/src/AuthenticatedSwitch.jsx @@ -14,6 +14,7 @@ import AdminLogs from "./pages/AdminLogs"; import ReportEncounter from "./pages/ReportsAndManagamentPages/ReportEncounter"; import ReportConfirm from "./pages/ReportsAndManagamentPages/ReportConfirm"; import ProjectList from "./pages/ProjectList"; +import ManualAnnotation from "./pages/ManualAnnotation"; export default function AuthenticatedSwitch({ showAlert, @@ -67,6 +68,10 @@ export default function AuthenticatedSwitch({ } /> } /> } /> + } /> + } /> } /> } /> diff --git a/frontend/src/components/ResizableRotatableRect.jsx b/frontend/src/components/ResizableRotatableRect.jsx new file mode 100644 index 0000000000..58484a5c67 --- /dev/null +++ b/frontend/src/components/ResizableRotatableRect.jsx @@ -0,0 +1,114 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Stage, Layer, Rect, Transformer } from "react-konva"; + +const ResizableRotatableRect = ({ + rect, + imgHeight, + imgWidth, + setRect, + setValue, + drawStatus, +}) => { + const [rectProps, setRectProps] = useState({}); + + useEffect(() => { + if (drawStatus !== "DELETE") { + setRectProps( + { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + fill: null, + stroke: "red", + strokeWidth: 2, + draggable: true, + } + ); + } + }, [rect.x, rect.y, rect.width, rect.height, drawStatus]); + + + const rectRef = useRef(null); + const transformerRef = useRef(null); + + const handleTransform = () => { + + const node = rectRef.current; + const scaleX = node.scaleX(); + const scaleY = node.scaleY(); + + const newWidth = Math.max(5, node.width() * scaleX); + const newHeight = Math.max(5, node.height() * scaleY); + + const updatedRect = { + x: node.x(), + y: node.y(), + width: newWidth, + height: newHeight, + rotation: node.rotation(), + }; + setRectProps({ + ...rectProps, + ...updatedRect, + }); + + setRect({ + ...rect, + ...updatedRect, + }); + + node.scaleX(1); + node.scaleY(1); + setValue(node.rotation()); + console.log("x after resizing: ", node.x()); + console.log("y after resizing:", node.y()); + }; + + const handleDragEnd = () => { + const node = rectRef.current; + const updatedRect = { + x: node.x(), + y: node.y(), + }; + + setRectProps({ + ...rectProps, + ...updatedRect, + }); + + setRect({ + ...rectProps, + ...updatedRect, + }); + }; + + const handleSelect = (e) => { + transformerRef.current.nodes([rectRef.current]); + transformerRef.current.getLayer().batchDraw(); + }; + + return ( + + + + + + + ); +}; + +export default ResizableRotatableRect; diff --git a/frontend/src/components/Slider.jsx b/frontend/src/components/Slider.jsx new file mode 100644 index 0000000000..86b7e8dd5e --- /dev/null +++ b/frontend/src/components/Slider.jsx @@ -0,0 +1,92 @@ +import React, { useState, useContext } from "react"; +import Slider from "rc-slider"; +import "rc-slider/assets/index.css"; +import ThemeColorContext from "../ThemeColorProvider"; + +function RotationSlider({ setValue }) { + const [angle, setAngle] = useState(0); + const theme = useContext(ThemeColorContext); + + return ( +
+ { + setAngle(value); + setValue(value); + }} + marks={{ + "-360": "-360°", + "-270": "-270°", + "-180": "-180°", + "-90": "-90°", + 0: "0°", + 90: "90°", + 180: "180°", + 270: "270°", + 360: "360°", + }} + railStyle={{ backgroundColor: "transparent", height: 8 }} + trackStyle={{ backgroundColor: "transparent", height: 8 }} + handleStyle={{ + backgroundColor: theme.primaryColors.primary500, + borderColor: theme.primaryColors.primary500, + width: "8px", + height: "20px", + borderRadius: "4px", + marginTop: -8, + // backgroundColor: "#fff", + }} + dots={true} + dotStyle={(dotValue) => { + if (angle > 0 && angle <= 180 && dotValue <= angle && dotValue > 0) { + return { + backgroundColor: theme.primaryColors.primary500, + borderColor: theme.primaryColors.primary500, + width: "8px", + height: "8px", + }; + } + + if (angle >= -180 && angle < 0 && dotValue >= angle && dotValue < 0) { + return { + backgroundColor: theme.primaryColors.primary500, + borderColor: theme.primaryColors.primary500, + width: "8px", + height: "8px", + }; + } + + if (dotValue % 90 === 0) { + return { + backgroundColor: theme.grayColors.gray200, + borderColor: theme.grayColors.gray200, + width: "6px", + height: "12px", + borderRadius: "4px", + }; + } + if (dotValue === 0) { + return { + backgroundColor: theme.primaryColors.primary500, + width: "14px", + height: "14px", + }; + } + + return { + backgroundColor: theme.grayColors.gray200, + borderColor: theme.grayColors.gray200, + width: "8px", + height: "8px", + }; + }} + /> +
+ ); +} + +export default RotationSlider; diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 1be96402cd..5acd336e46 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -328,5 +328,7 @@ "ITEMS" : "Artikel", "INPUT_PAGE_ALERT" : "Bitte geben Sie eine gültige Seitenzahl zwischen 1 und {totalPages}.", "NO_PROJECTS" : "Keine Projekte", - "PROJECT_LIST_TITLE": "Wildbook – Meine Projekte" + "PROJECT_LIST_TITLE": "Wildbook – Meine Projekte", + "ADD_ANNOTATIONS": "Annotationen hinzufügen", + "SAVE_ANNOTATION" : "Annotation speichern" } diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index af7f243530..4609b53b7e 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -328,5 +328,7 @@ "ITEMS" : "items", "INPUT_PAGE_ALERT" : "Please enter a valid page number between 1 and {totalPages}.", "NO_PROJECTS" : "No Projects", - "PROJECT_LIST_TITLE": "Wildbook - My Projects" + "PROJECT_LIST_TITLE": "Wildbook - My Projects", + "ADD_ANNOTATIONS" : "Add Annotations", + "SAVE_ANNOTATION" : "Save Annotation" } \ No newline at end of file diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 0ed7ddc6c0..bea5cc1429 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -327,5 +327,7 @@ "ITEMS" : "elementos", "INPUT_PAGE_ALERT" : "Por favor, introduzca un número de página válido entre 1 y {totalPages}.", "NO_PROJECTS" : "Sin proyectos", - "PROJECT_LIST_TITLE": "Wildbook - Mis Proyectos" + "PROJECT_LIST_TITLE": "Wildbook - Mis Proyectos", + "ADD_ANNOTATIONS": "Añadir anotaciones", + "SAVE_ANNOTATION" : "Guardar anotación" } \ No newline at end of file diff --git a/frontend/src/locale/fr.json b/frontend/src/locale/fr.json index b72d20e9b8..8effac98ee 100644 --- a/frontend/src/locale/fr.json +++ b/frontend/src/locale/fr.json @@ -327,5 +327,7 @@ "ITEMS" : "articles", "INPUT_PAGE_ALERT" : "Veuillez saisir un numéro de page valide entre 1 et {totalPages}.", "NO_PROJECTS" : "Aucun projet", - "PROJECT_LIST_TITLE": "Wildbook - Mes projets" + "PROJECT_LIST_TITLE": "Wildbook - Mes projets", + "ADD_ANNOTATIONS": "Ajouter des annotations", + "SAVE_ANNOTATION" : "Enregistrer l'annotation" } \ No newline at end of file diff --git a/frontend/src/locale/it.json b/frontend/src/locale/it.json index c8b9c75cf4..c1ac7761c9 100644 --- a/frontend/src/locale/it.json +++ b/frontend/src/locale/it.json @@ -327,5 +327,7 @@ "ITEMS" : "elementi", "INPUT_PAGE_ALERT" : "Inserisci un numero di pagina valido compreso tra 1 e {totalPages}.", "NO_PROJECTS" : "Nessun progetto", - "PROJECT_LIST_TITLE": "Wildbook - I miei progetti" + "PROJECT_LIST_TITLE": "Wildbook - I miei progetti", + "ADD_ANNOTATIONS": "Aggiungi annotazioni", + "SAVE_ANNOTATION" : "Salva annotazione" } \ No newline at end of file diff --git a/frontend/src/pages/ManualAnnotation.jsx b/frontend/src/pages/ManualAnnotation.jsx new file mode 100644 index 0000000000..f5b3967697 --- /dev/null +++ b/frontend/src/pages/ManualAnnotation.jsx @@ -0,0 +1,410 @@ +import React, { useState, useContext, useRef, useEffect } from "react"; +import Select from "react-select"; +import Form from "react-bootstrap/Form"; +import { FormattedMessage } from "react-intl"; +import Container from "react-bootstrap/Container"; +import MainButton from "../components/MainButton"; +import ThemeColorContext from "../ThemeColorProvider"; +import ResizableRotatableRect from "../components/ResizableRotatableRect"; +import useGetSiteSettings from "../models/useGetSiteSettings"; +import axios from "axios"; +import { useSearchParams } from "react-router-dom"; +import Modal from "react-bootstrap/Modal"; +import Button from "react-bootstrap/Button"; +import AnnotationSuccessful from "../components/AnnotationSuccessful"; + +export default function ManualAnnotation() { + + const [searchParams] = useSearchParams(); + const assetId = searchParams.get("assetId"); + const encounterId = searchParams.get("encounterId"); + const theme = useContext(ThemeColorContext); + const imgRef = useRef(null); + const [value, setValue] = useState(0); + const [data, setData] = useState({ + width: 100, + height: 100, + url: "" + }); + + const [showModal, setShowModal] = useState(false); // State to manage Modal visibility + const [submissionDone, setsubmissionDone] = useState(false); // State to manage submission status + + const { data: siteData } = useGetSiteSettings(); + + const iaOptions = siteData?.iaClass?.map((iaClass) => ({ + value: iaClass, + label: iaClass, + })); + + const viewpointOptions = siteData?.annotationViewpoint?.map((viewpoint) => ({ + value: viewpoint, + label: viewpoint, + })); + + const [ia, setIa] = useState(null); + const [viewpoint, setViewpoint] = useState(null); + + const [rect, setRect] = useState({ + x: 0, + y: 0, + width: 0, + height: 0, + rotation: 0, + }); + const [isDrawing, setIsDrawing] = useState(false); + const [drawStatus, setDrawStatus] = useState("DRAW"); + + const getMediaAssets = async () => { + try { + const response = await fetch(`/api/v3/media-assets/${assetId}`); + const data = await response.json(); + setData(data); + } catch (error) { + } + }; + + console.log("rect", JSON.stringify(rect)); + + const [scaleFactor, setScaleFactor] = useState({ x: 1, y: 1 }); + + // useEffect(() => { + // if (rect.width === 0 || rect.height === 0 || value === 0) { + // return; + // } + // const radians = (value * Math.PI) / 180; + // const halfW = rect.width / 2; + // const halfH = rect.height / 2; + + // const theta0 = Math.atan(halfH / halfW); + // const radius = Math.sqrt(halfW * halfW + halfH * halfH); + // //console.log('jon halfW=%d halfH=%d theta0=%o radius=%o', halfW * scaleFactor.x, halfH * scaleFactor.y, theta0, radius * scaleFactor.y); + + // const a = Math.cos(radians + theta0) * radius; + // const b = Math.sin(radians + theta0) * radius; + // //console.log('radians=%o jon a=%o b=%o', radians, a * scaleFactor.x, b * scaleFactor.y); + // //console.log('jon: rx, ry (%d,%d)', rect.x * scaleFactor.x, rect.y * scaleFactor.y); + + // const cx = rect.x + a; + // const cy = rect.y + b; + // console.log(">>>> jon cx and cy: (%d, %d)", cx * scaleFactor.x, cy * scaleFactor.y); + + // const x = cx - halfW; + // const y = cy - halfH; + // console.log(">>>> jon x and y: (%d, %d)", x * scaleFactor.x, y * scaleFactor.y); + + // /* + // const radian = (Math.PI / 180) * value; + + // const centerX = rect.x + rect.width / 2; + // const centerY = rect.y + rect.height / 2; + + // const dx = -rect.width / 2; + // const dy = -rect.height / 2; + // const originalDx = dx * Math.cos(-radian) - dy * Math.sin(-radian); + // const originalDy = dx * Math.sin(-radian) + dy * Math.cos(-radian); + + // const originalX = centerX + originalDx; + // const originalY = centerY + originalDy; + // */ + // /* + // console.log("x and y after rotation:", rect.x, rect.y); + // console.log("x and y after scale:", rect.x * scaleFactor.x, rect.y * scaleFactor.y); + // console.log("#1 ...jon... x.. and y.. before rotation:", x, y); + // console.log("#2 ...erin... x.. and y.. before rotation:", originalX, originalY); + // */ + // }, [value, rect]); + + useEffect(() => { + if (isDrawing) { + setDrawStatus("DRAWING"); + } else if (rect.width > 0 && rect.height > 0) { + setDrawStatus("DELETE"); + } else { + setDrawStatus("DRAW"); + } + }, [isDrawing, rect]); + + useEffect(() => { + const handleImageLoad = () => { + if (imgRef.current) { + const naturalWidth = data.width; + const naturalHeight = data.height; + // const naturalWidth = imgRef.current.naturalWidth; + // const naturalHeight = imgRef.current.naturalHeight; + const displayWidth = imgRef.current.clientWidth; + const displayHeight = imgRef.current.clientHeight; + + const scaleX = naturalWidth / displayWidth; + const scaleY = naturalHeight / displayHeight; + + setScaleFactor({ x: scaleX, y: scaleY }); + } + }; + + const imgElement = imgRef.current; + if (imgElement && imgElement.complete) { + handleImageLoad(); + } else if (imgElement) { + imgElement.addEventListener("load", handleImageLoad); + } + + return () => { + if (imgElement) { + imgElement.removeEventListener("load", handleImageLoad); + } + }; + }, [data]); + + useEffect(() => { + if (assetId && encounterId) { + const fetchData = async () => { + await getMediaAssets(); + }; + fetchData(); + } + }, [assetId, encounterId]); + + useEffect(() => { + const handleMouseUp = () => setIsDrawing(false); + window.addEventListener("mouseup", handleMouseUp); + return () => window.removeEventListener("mouseup", handleMouseUp); + }, []); + + const handleMouseDown = (e) => { + if (!imgRef.current || drawStatus === "DELETE") return; + + const { left, top } = imgRef.current.getBoundingClientRect(); + setRect({ + x: e.clientX - left, + y: e.clientY - top, + width: 0, + height: 0, + rotation: value, + }); + setIsDrawing(true); + }; + + const handleMouseMove = (e) => { + if (!imgRef.current || drawStatus === "DELETE") return; + + const { left, top } = imgRef.current.getBoundingClientRect(); + const mouseX = e.clientX - left; + const mouseY = e.clientY - top; + + if (isDrawing) { + setRect((prevRect) => ({ + ...prevRect, + width: mouseX - prevRect.x, + height: mouseY - prevRect.y, + rotation: value, + })); + } + }; + + const handleMouseUp = () => { + if (!imgRef.current || drawStatus === "DELETE") return; + setIsDrawing(false); + }; + + return ( + + + {submissionDone ? + : <>

+ +

+
+ + + * + + { + setViewpoint(selected) + }} + /> + +
+
+
+
+ +
+
{ + if (drawStatus === "DELETE") { + setRect({ + x: 0, + y: 0, + width: 0, + height: 0, + }); + } else if (drawStatus === "DRAW") { + setDrawStatus("DRAWING"); + } + }} + > + + +
+
+
+ annotationimages + + +
+ +
+ + { + try { + if (!ia || !viewpoint || !rect.width || !rect.height) { + setShowModal(true); + return; + } else { + setShowModal(false); + // setsubmissionDone(true); + } + // const radians = (value * Math.PI) / 180; + // const originalX = rect.x * Math.cos(radians) + rect.y * Math.sin(radians); + // const originalY = -rect.x * Math.sin(radians) + rect.y * Math.cos(radians); + + const radians = (value * Math.PI) / 180; + const halfW = rect.width / 2; + const halfH = rect.height / 2; + + const theta0 = Math.atan(halfH / halfW); + const radius = Math.sqrt(halfW * halfW + halfH * halfH); + //console.log('jon halfW=%d halfH=%d theta0=%o radius=%o', halfW * scaleFactor.x, halfH * scaleFactor.y, theta0, radius * scaleFactor.y); + + const a = Math.cos(radians + theta0) * radius; + const b = Math.sin(radians + theta0) * radius; + //console.log('radians=%o jon a=%o b=%o', radians, a * scaleFactor.x, b * scaleFactor.y); + //console.log('jon: rx, ry (%d,%d)', rect.x * scaleFactor.x, rect.y * scaleFactor.y); + + const cx = rect.x + a; + const cy = rect.y + b; + console.log(">>>> jon cx and cy: (%d, %d)", cx * scaleFactor.x, cy * scaleFactor.y); + + const x = cx - halfW; + const y = cy - halfH; + console.log(">>>> jon x and y: (%d, %d)", x * scaleFactor.x, y * scaleFactor.y); + + + const response = await axios.request({ + method: "post", + url: "/api/v3/annotations", + data: { + "encounterId": encounterId, + "height": rect.height * scaleFactor.y, + "iaClass": ia.value, + "mediaAssetId": assetId, + "theta": (value * Math.PI) / 180, + "viewpoint": viewpoint.value, + "width": rect.width * scaleFactor.x, + "x": x * scaleFactor.x, + "y": y * scaleFactor.y, + + }, + }); + + const data = await response.json(); + setData(data); + } catch (error) { + } + + }} + > + + + setShowModal(false)}> + + + + + + + + + + + + } +
+ ); +} diff --git a/src/main/webapp/javascript/ia.IBEIS.js b/src/main/webapp/javascript/ia.IBEIS.js index d5e92f4457..5da34479bf 100644 --- a/src/main/webapp/javascript/ia.IBEIS.js +++ b/src/main/webapp/javascript/ia.IBEIS.js @@ -71,7 +71,7 @@ wildbook.IA.plugins.push({ function(enh) { //the menu action var mid = imageEnhancer.mediaAssetIdFromElement(enh.imgEl); var ma = assetById(mid); - wildbook.openInTab('manualAnnotation.jsp?encounterId=' + encounterNumberFromElement(enh.imgEl) + '&assetId=' + mid); + wildbook.openInTab('/react/manual-annotation?encounterId=' + encounterNumberFromElement(enh.imgEl) + '&assetId=' + mid); } ]);