diff --git a/frontend/src/components/map/legend.tsx b/frontend/src/components/map/legend.tsx
index 0f0ad4aa..631356af 100644
--- a/frontend/src/components/map/legend.tsx
+++ b/frontend/src/components/map/legend.tsx
@@ -22,7 +22,7 @@ const FillLegendStyle = ({
};
const Legend = () => {
- const [expandLegend, setExpandLegend] = useState(true);
+ const [expandLegend, setExpandLegend] = useState(false);
const { map } = useMap();
const activeLayers = map
diff --git a/frontend/src/contents/toast-notifications.ts b/frontend/src/contents/toast-notifications.ts
index c54155d3..b22391a3 100644
--- a/frontend/src/contents/toast-notifications.ts
+++ b/frontend/src/contents/toast-notifications.ts
@@ -10,6 +10,9 @@ export const TOAST_NOTIFICATIONS = {
approvedPrediction: {
success: "Saved successfully.",
},
+ resolved: {
+ success: "Action resolved successfully.",
+ },
modelPrediction: {
success: "Model predictions retrieved successfully.",
},
diff --git a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
index 85b0b000..c71d24cf 100644
--- a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
+++ b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
@@ -228,9 +228,9 @@ const TrainingAreaItem: React.FC<
? "Fetching labels..."
: trainingArea.properties.label_fetched !== null
? truncateString(
- `Fetched ${timeSinceLabelFetch === "0 sec" ? "just now" : `${timeSinceLabelFetch} ago`}`,
- 20,
- )
+ `Fetched ${timeSinceLabelFetch === "0 sec" ? "just now" : `${timeSinceLabelFetch} ago`}`,
+ 20,
+ )
: "No labels yet"}
diff --git a/frontend/src/features/start-mapping/api/create-feedbacks.ts b/frontend/src/features/start-mapping/api/create-feedbacks.ts
index d28f0c60..e319f00c 100644
--- a/frontend/src/features/start-mapping/api/create-feedbacks.ts
+++ b/frontend/src/features/start-mapping/api/create-feedbacks.ts
@@ -17,7 +17,7 @@ export const createFeedback = async ({
source_imagery,
zoom_level,
training,
-}: TCreateFeedbackPayload): Promise => {
+}: TCreateFeedbackPayload): Promise => {
return await (
await apiClient.post(API_ENDPOINTS.CREATE_FEEDBACK, {
comments,
@@ -42,7 +42,7 @@ export const createApprovedPrediction = async ({
geom,
training,
user,
-}: TCreateApprovedPredictionPayload): Promise => {
+}: TCreateApprovedPredictionPayload): Promise => {
return await (
await apiClient.post(API_ENDPOINTS.CREATE_APPROVED_PREDICTION, {
config,
@@ -52,3 +52,29 @@ export const createApprovedPrediction = async ({
})
).data;
};
+
+export type TDeleteModelPredictionFeedbackPayload = {
+ id: number;
+ approvePrediction?: boolean;
+};
+
+export const deleteModelPredictionFeedback = async ({
+ id,
+}: TDeleteModelPredictionFeedbackPayload) => {
+ return await (
+ await apiClient.delete(API_ENDPOINTS.DELETE_FEEDBACK(id))
+ ).data;
+};
+
+export type TDeleteApprovedModelPredictionPayload = {
+ id: number;
+ createFeedback?: boolean;
+};
+
+export const deleteApprovedModelPrediction = async ({
+ id,
+}: TDeleteApprovedModelPredictionPayload) => {
+ return await (
+ await apiClient.delete(API_ENDPOINTS.DELETE_APPROVED_PREDICTION(id))
+ ).data;
+};
diff --git a/frontend/src/features/start-mapping/components/model-action.tsx b/frontend/src/features/start-mapping/components/model-action.tsx
index 2dbfcac3..0f7d0265 100644
--- a/frontend/src/features/start-mapping/components/model-action.tsx
+++ b/frontend/src/features/start-mapping/components/model-action.tsx
@@ -1,4 +1,4 @@
-import { Feature, TModelPredictions } from "@/types";
+import { TModelPredictions } from "@/types";
import {
MIN_ZOOM_LEVEL_FOR_PREDICTION,
showErrorToast,
diff --git a/frontend/src/features/start-mapping/components/popup.tsx b/frontend/src/features/start-mapping/components/popup.tsx
index 86064f81..cad84dba 100644
--- a/frontend/src/features/start-mapping/components/popup.tsx
+++ b/frontend/src/features/start-mapping/components/popup.tsx
@@ -8,6 +8,8 @@ import { SHOELACE_SIZES } from "@/enums";
import {
useCreateApprovedModelPrediction,
useCreateModelFeedback,
+ useDeleteApprovedModelPrediction,
+ useDeleteModelPredictionFeedback,
} from "@/features/start-mapping/hooks/use-feedbacks";
import { showErrorToast, showSuccessToast } from "@/utils";
import { geojsonToWKT } from "@terraformer/wkt";
@@ -43,34 +45,41 @@ const PredictedFeatureActionPopup = ({
const popupRef = useRef(null);
const [popup, setPopup] = useState(null);
const { accepted, rejected, all } = modelPredictions;
+ const { isMobile } = useScreenSize();
const alreadyAccepted = accepted.some(
(feature) => feature.properties.id === featureId,
);
const alreadyRejected = rejected.some(
(feature) => feature.properties.id === featureId,
);
+
// if already accepted, it means it's in accepted array
// if it's already rejected, it means it's in the rejected array
// if it's not in accepted or rejected, then it's in the all array
- const feature = alreadyAccepted
- ? modelPredictions.accepted.filter(
- (feature) => feature.properties.id === featureId,
- )[0]
- : alreadyRejected
- ? modelPredictions.rejected.filter(
- (feature) => feature.properties.id === featureId,
- )[0]
- : modelPredictions.all.filter(
- (feature) => feature.properties.id === featureId,
- )[0];
+ const feature =
+ accepted.find((f) => f.properties.id === featureId) ||
+ rejected.find((f) => f.properties.id === featureId) ||
+ all.find((f) => f.properties.id === featureId);
const [showComment, setShowComment] = useState(false);
const [comment, setComment] = useState("");
- const moveFeature = (source: Feature[], target: Feature[], id: string) => {
- const movedFeatures = source.filter(
- (feature) => feature.properties.id === id,
- );
+ const moveFeature = (
+ source: Feature[],
+ target: Feature[],
+ id: string,
+ additionalProperties: Partial = {},
+ ) => {
+ const movedFeatures = source
+ .filter((feature) => feature.properties.id === id)
+ .map((feature) => ({
+ ...feature,
+ properties: {
+ ...feature.properties,
+ ...additionalProperties,
+ },
+ }));
+
return {
updatedSource: source.filter((feature) => feature.properties.id !== id),
updatedTarget: [...target, ...movedFeatures],
@@ -93,40 +102,36 @@ const PredictedFeatureActionPopup = ({
const closePopup = () => {
popup?.remove();
- // clean ups
setShowComment(false);
setComment("");
};
+ const handleRejection = () => {
+ setShowComment(true);
+ };
+
// Approved prediction is accept
const createApprovedModelPredictionMutation =
useCreateApprovedModelPrediction({
mutationConfig: {
- onSuccess: () => {
- if (alreadyRejected) {
- const { updatedSource, updatedTarget } = moveFeature(
- rejected,
- accepted,
- featureId,
- );
- setModelPredictions((prev) => ({
- ...prev,
- rejected: updatedSource,
- accepted: updatedTarget,
- }));
- } else {
- const { updatedSource, updatedTarget } = moveFeature(
- all,
- accepted,
- featureId,
- );
- setModelPredictions((prev) => ({
- ...prev,
- all: updatedSource,
- accepted: updatedTarget,
- }));
- }
+ onSuccess: (data) => {
+ const { updatedSource, updatedTarget } = alreadyRejected
+ ? moveFeature(rejected, accepted, featureId, {
+ _id: data.id,
+ ...data.properties,
+ })
+ : moveFeature(all, accepted, featureId, {
+ _id: data.id,
+ ...data.properties,
+ });
+
+ setModelPredictions((prev) => ({
+ ...prev,
+ rejected: alreadyRejected ? updatedSource : prev.rejected,
+ all: alreadyRejected ? prev.all : updatedSource,
+ accepted: updatedTarget,
+ }));
closePopup();
showSuccessToast(
TOAST_NOTIFICATIONS.startMapping.approvedPrediction.success,
@@ -138,15 +143,97 @@ const PredictedFeatureActionPopup = ({
},
});
+ const deleteModelFeedbackMutation = useDeleteModelPredictionFeedback({
+ mutationConfig: {
+ onSuccess: (_, variables) => {
+ if (variables.approvePrediction) {
+ submitApprovedPrediction();
+ } else {
+ const { updatedSource: updatedRejected } = moveFeature(
+ rejected,
+ all,
+ featureId,
+ );
+ setModelPredictions((prev) => ({
+ ...prev,
+ all: [
+ ...all,
+ ...rejected.filter((f) => f.properties.id === featureId),
+ ],
+ rejected: updatedRejected,
+ }));
+ }
+ showSuccessToast(TOAST_NOTIFICATIONS.startMapping.resolved.success);
+ },
+ onError: (error) => {
+ showErrorToast(error);
+ },
+ },
+ });
+
+ const deleteApprovedModelPrediction = useDeleteApprovedModelPrediction({
+ mutationConfig: {
+ onSuccess: async (_, variables) => {
+ if (variables.createFeedback) {
+ await createModelFeedbackMutation.mutateAsync({
+ zoom_level: trainingConfig.zoom_level,
+ comments: comment,
+ geom: geojsonToWKT(feature?.geometry as GeoJSONType),
+ feedback_type: "TN",
+ source_imagery: source_imagery,
+ training: trainingId,
+ });
+ } else {
+ const { updatedSource: updatedAccepted } = moveFeature(
+ accepted,
+ all,
+ featureId,
+ );
+ setModelPredictions((prev) => ({
+ ...prev,
+ all: [
+ ...all,
+ ...accepted.filter((f) => f.properties.id === featureId),
+ ],
+ accepted: updatedAccepted,
+ }));
+ }
+ showSuccessToast(TOAST_NOTIFICATIONS.startMapping.resolved.success);
+ },
+ onError: (error) => {
+ showErrorToast(error);
+ },
+ },
+ });
+
+ const submitApprovedPrediction = async () => {
+ await createApprovedModelPredictionMutation.mutateAsync({
+ geom: geojsonToWKT(feature?.geometry as GeoJSONType),
+ training: trainingId,
+ config: {
+ areathreshold: Number(trainingConfig.area_threshold),
+ confidence: trainingConfig.confidence,
+ josmq: trainingConfig.use_josm_q,
+ maxanglechange: trainingConfig.max_angle_change,
+ skewtolerance: trainingConfig.skew_tolerance,
+ tolerance: trainingConfig.tolerance,
+ zoomlevel: trainingConfig.zoom_level,
+ },
+ user: user.osm_id,
+ });
+ };
+
// Rejection is the same as feedback
const createModelFeedbackMutation = useCreateModelFeedback({
mutationConfig: {
- onSuccess: () => {
+ onSuccess: (data) => {
if (alreadyAccepted) {
const { updatedSource, updatedTarget } = moveFeature(
accepted,
rejected,
featureId,
+ // update the feature with the returned id from the backend as `_id` and other properties from the backend.
+ { _id: data.id, ...data.properties },
);
setModelPredictions((prev) => ({
...prev,
@@ -158,6 +245,8 @@ const PredictedFeatureActionPopup = ({
all,
rejected,
featureId,
+ // update the feature with the returned id from the backend as `_id` and other properties from the backend.
+ { _id: data.id, ...data.properties },
);
setModelPredictions((prev) => ({
...prev,
@@ -175,60 +264,45 @@ const PredictedFeatureActionPopup = ({
});
const submitRejectionFeedback = async () => {
- await createModelFeedbackMutation.mutateAsync({
- zoom_level: trainingConfig.zoom_level,
- comments: comment,
- geom: geojsonToWKT(feature.geometry as GeoJSONType),
- feedback_type: "TN",
- source_imagery: source_imagery,
- training: trainingId,
- });
- };
-
- const handleRejection = () => {
- setShowComment(true);
+ if (alreadyAccepted) {
+ await deleteApprovedModelPrediction.mutateAsync({
+ id: feature?.properties._id,
+ createFeedback: true,
+ });
+ } else {
+ await createModelFeedbackMutation.mutateAsync({
+ zoom_level: trainingConfig.zoom_level,
+ comments: comment,
+ geom: geojsonToWKT(feature?.geometry as GeoJSONType),
+ feedback_type: "TN",
+ source_imagery: source_imagery,
+ training: trainingId,
+ });
+ }
};
- const handleResolve = () => {
- const { updatedSource: updatedRejected } = moveFeature(
- rejected,
- all,
- featureId,
- );
- const { updatedSource: updatedAccepted } = moveFeature(
- accepted,
- all,
- featureId,
- );
- setModelPredictions((prev) => ({
- ...prev,
- all: [
- ...all,
- ...rejected.filter((f) => f.properties.id === featureId),
- ...accepted.filter((f) => f.properties.id === featureId),
- ],
- rejected: updatedRejected,
- accepted: updatedAccepted,
- }));
-
+ const handleResolve = async () => {
+ if (alreadyRejected) {
+ await deleteModelFeedbackMutation.mutateAsync({
+ id: feature?.properties._id,
+ });
+ } else if (alreadyAccepted) {
+ await deleteApprovedModelPrediction.mutateAsync({
+ id: feature?.properties._id,
+ });
+ }
closePopup();
};
const handleAcceptance = async () => {
- await createApprovedModelPredictionMutation.mutateAsync({
- geom: geojsonToWKT(feature.geometry as GeoJSONType),
- training: trainingId,
- config: {
- areathreshold: Number(trainingConfig.area_threshold),
- confidence: trainingConfig.confidence,
- josmq: trainingConfig.use_josm_q,
- maxanglechange: trainingConfig.max_angle_change,
- skewtolerance: trainingConfig.skew_tolerance,
- tolerance: trainingConfig.tolerance,
- zoomlevel: trainingConfig.zoom_level,
- },
- user: user.osm_id,
- });
+ if (alreadyRejected) {
+ await deleteModelFeedbackMutation.mutateAsync({
+ id: feature?.properties._id,
+ approvePrediction: true,
+ });
+ } else {
+ submitApprovedPrediction();
+ }
};
const primaryButton = alreadyAccepted
@@ -272,7 +346,7 @@ const PredictedFeatureActionPopup = ({
className: "bg-primary",
icon: RejectIcon,
};
- const { isMobile } = useScreenSize();
+
return (