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 ?
+ : <>
+
+
+
+
+ *
+
+
+
+
+ *
+
+
+
+
+
+
+
+
+
{
+ if (drawStatus === "DELETE") {
+ setRect({
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ });
+ } else if (drawStatus === "DRAW") {
+ setDrawStatus("DRAWING");
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ 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);
}
]);