diff --git a/client/src/Annotator/index.jsx b/client/src/Annotator/index.jsx index 514640b..da795d9 100644 --- a/client/src/Annotator/index.jsx +++ b/client/src/Annotator/index.jsx @@ -15,7 +15,7 @@ import PropTypes from "prop-types" import noopReducer from "./reducers/noop-reducer.js" import {useTranslation} from "react-i18next" import getActiveImage from "./reducers/get-active-image.js" -import { saveActiveImage } from "../utils/send-data-to-server" +import { saveActiveImage, saveData, splitRegionData, getImageData} from "../utils/send-data-to-server" import { useSnackbar} from "../SnackbarContext/index.jsx" export const Annotator = ({ images, @@ -124,21 +124,55 @@ export const Annotator = ({ showSnackbar(error.message, 'error'); }); } - - const dispatch = useEventCallback((action) => { + const preprocessDataBeforeSend = async (output) => { + const selectedImageIndex = output.selectedImage; + let _image = output.images[selectedImageIndex]; + let regions = _image['regions'] || []; + let imageData = getImageData(_image); + + imageData['regions'] = []; + for (let regionNum = 0; regionNum < regions.length; regionNum++) { + imageData['regions'].push(splitRegionData(regions[regionNum])); + } + + try { + const response = await saveData(imageData); + showSnackbar(response.message, 'success'); + return imageData['regions']; + } catch (error) { + showSnackbar(error.message, 'error'); + return []; + } + }; + + const dispatch = useEventCallback(async (action) => { if (action.type === "HEADER_BUTTON_CLICKED") { if (["Exit", "Done", "Save", "Complete"].includes(action.buttonName)) { - return onExit(without(state, "history")) + // save the current data + if (action.buttonName === "Save") { + const result = await preprocessDataBeforeSend(without(state, "history")); + dispatchToReducer({ + type: "SAVE_LAST_REGIONS", + payload: result + }); + dispatchToReducer({ + type: "ENABLE_DOWNLOAD", + payload: result + }); + return null; + } else { + return onExit(without(state, "history")); + } } else if (action.buttonName === "Next" && onNextImage) { - saveCurrentData(getActiveImage(state).activeImage) - return onNextImage(without(state, "history")) + saveCurrentData(getActiveImage(state).activeImage); + return onNextImage(without(state, "history")); } else if (action.buttonName === "Prev" && onPrevImage) { - saveCurrentData(getActiveImage(state).activeImage) - return onPrevImage(without(state, "history")) + saveCurrentData(getActiveImage(state).activeImage); + return onPrevImage(without(state, "history")); } } - dispatchToReducer(action) - }) + dispatchToReducer(action); + }); const onRegionClassAdded = useEventCallback((cls) => { dispatchToReducer({ diff --git a/client/src/DemoSite/index.jsx b/client/src/DemoSite/index.jsx index 4a8edd4..c27bfc6 100644 --- a/client/src/DemoSite/index.jsx +++ b/client/src/DemoSite/index.jsx @@ -1,31 +1,50 @@ import Annotator from "../Annotator" import React, { useEffect, useState } from "react" -import {saveData, splitRegionData, getImageData} from '../utils/send-data-to-server' import SetupPage from "../SetupPage"; -import { useSnackbar } from '../SnackbarContext'; import { useSettings } from "../SettingsProvider"; +import {setIn} from "seamless-immutable" + +const extractRelevantProps = (region) => ({ + cls: region.cls, + comment: region.comment, + id: region.id, +}); const userReducer = (state, action) => { switch (action.type) { - // case "SELECT_CLASSIFICATION": { - // switch (action.cls) { - // case "One": { - // return setIn(state, ["selectedTool"], "create-box"); - // } - // case "Two": { - // return setIn(state, ["selectedTool"], "create-polygon"); - // } - // } - // } + case "CLOSE_REGION_EDITOR": + case "DELETE_REGION": { + const { images, selectedImage } = state; + const lastRegions = state.lastRegions || []; + if (selectedImage != null && lastRegions) { + const currentImage = images[selectedImage]; + const regions = currentImage ? (currentImage.regions || []) : []; + if ( + regions.length !== lastRegions.length || + !regions.every((region, index) => { + const lastRegion = lastRegions[index] || []; + const currentProps = extractRelevantProps(region); + const lastProps = extractRelevantProps(lastRegion); + return JSON.stringify(currentProps) === JSON.stringify(lastProps); + }) + ) { + return setIn(state, ["hasNewChange"], true); + } else { + return setIn(state, ["hasNewChange"], false); + } + } + } + case "SAVE_LAST_REGIONS": { + return setIn(state, ["lastRegions"], action.payload); + } + case "ENABLE_DOWNLOAD": { + return setIn(state, ["enabledDownload"], true); + } } - return state; }; - - export default () => { - const { showSnackbar } = useSnackbar(); const [selectedImageIndex, changeSelectedImageIndex] = useState(0) const [showLabel, setShowLabel] = useState(false) const [imageNames, setImageNames] = useState([]) @@ -42,23 +61,6 @@ export default () => { } }) - const preprocessDataBeforeSend = (output) => { - const selectedImageIndex = output.selectedImage; - let _image = output.images[selectedImageIndex] - let regions = _image['regions'] || [] - let imageData = getImageData(_image) - - imageData['regions'] = [] - for (let regionNum = 0; regionNum < regions.length; regionNum++){ - imageData['regions'].push(splitRegionData(regions[regionNum])) - } - saveData(imageData).then(response => { - showSnackbar(response.message, 'success'); - }) - .catch(error => { - showSnackbar(error.message, 'error'); - }); - } const [loading, setLoading] = useState(true); // Add loading state const onSelectJumpHandle = (selectedImageName) => { @@ -144,7 +146,7 @@ export default () => { enabledRegionProps= {["class", "comment"]} userReducer= {userReducer} onExit={(output) => { - preprocessDataBeforeSend(output) + console.log("Exiting!") }} settings={settings} onSelectJump={onSelectJumpHandle} diff --git a/client/src/MainLayout/index.jsx b/client/src/MainLayout/index.jsx index c59ebbf..d0243ac 100644 --- a/client/src/MainLayout/index.jsx +++ b/client/src/MainLayout/index.jsx @@ -224,6 +224,7 @@ export const MainLayout = ({ ) : null, ].filter(Boolean)} headerItems={[ + { name: "Download", label:t("btn.download"), disabled: !state.enabledDownload, hasInnerMenu: true}, !hidePrev && {name: "Prev", label: t("btn.previous"), disabled: disabledNextAndPrev}, !hideNext && {name: "Next", label: t("btn.next"), disabled: disabledNextAndPrev}, state.annotationType !== "video" @@ -231,10 +232,10 @@ export const MainLayout = ({ : !state.videoPlaying ? {name: "Play", label: t("btn.play")} : {name: "Pause", label: t("btn.pause")}, - !hideClone && + !hideClone && state.hasNewChange && !nextImageHasRegions && activeImage.regions && {name: "Clone", label: t("btn.clone")}, - !hideSave && {name: "Save", label:t("btn.save"), icon: }, + !hideSave && {name: "Save", label:t("btn.save"), disabled: !state.hasNewChange, icon: }, !hideSettings && {name: "Settings", label: t("btn.settings")}, {name: "Exit", label:t("btn.exit"), icon: } ].filter(Boolean)} diff --git a/client/src/SetupPage/index.jsx b/client/src/SetupPage/index.jsx index 57601aa..bef8ff1 100644 --- a/client/src/SetupPage/index.jsx +++ b/client/src/SetupPage/index.jsx @@ -125,7 +125,7 @@ export const SetupPage = ({setConfiguration, settings, setShowLabel}) => { {settings.taskChoice === "image_classification" && ( <> - + @@ -136,7 +136,7 @@ export const SetupPage = ({setConfiguration, settings, setShowLabel}) => { {settings.taskChoice === "image_segmentation" && ( <> - + diff --git a/client/src/workspace/DownloadButton/index.jsx b/client/src/workspace/DownloadButton/index.jsx index 56160ab..43952f9 100644 --- a/client/src/workspace/DownloadButton/index.jsx +++ b/client/src/workspace/DownloadButton/index.jsx @@ -12,7 +12,7 @@ import { hexToRgbTuple } from "../../utils/color-utils.js"; import HeaderButton from "../HeaderButton/index.jsx"; import { useTranslation } from "react-i18next" -const DownloadButton = ({selectedImageName, classList, hideHeaderText}) => { +const DownloadButton = ({selectedImageName, classList, hideHeaderText, disabled}) => { const [anchorEl, setAnchorEl] = useState(null); const { showSnackbar } = useSnackbar(); const {t} = useTranslation(); @@ -80,6 +80,7 @@ const DownloadButton = ({selectedImageName, classList, hideHeaderText}) => { name={"Download"} label={t("btn.download")} onClick={handleClick} + disabled={disabled} icon={} /> { const{ t } = useTranslation() - + const downloadMenu = items.find((item) => item.name === "Download") + return ( @@ -41,10 +42,11 @@ export const Header = ({ {t("labname")} {leftSideContent} - - {items.map((item) => ( + } + {items.filter(item => item.name !== "Download").map((item) => (