diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 50af99e601..6d3cf5237b 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -511,6 +511,7 @@ "ROW": "Zeile", "APPLY_TO_ALL_ROWS_NOTE": "+ kennzeichnet, dass die Änderung auf alle Zeilen dieser Spalte angewendet werden kann", "BULK_IMPORT_DELETE_TASK": "Massenimport-Aufgabe löschen", + "BULK_IMPORT_DELETE_TASK_IN_PROGRESS": "Wird gelöscht...", "BULK_IMPORT_DATA_UPLOADED": "Daten hochgeladen", "BULK_IMPORT_IMAGE_UPLOADED": "Bild hochgeladen", "SPREADSHEET_UPLOADED_TITLE": "Tabellen-Datei hochgeladen: {fileName}", @@ -764,6 +765,7 @@ "INDIVIDUAL_SCORE": "Individueller Score", "IMAGE_SCORE": "Bild-Score", "NUMBER_OF_RESULTS": "Anzahl der Ergebnisse", + "NUMBER_OF_RESULTS_MAX_HINT": "(max. {max})", "SELECT_A_PROJECT": "ein Projekt auswählen", "MATCHED_BASED_ON": "Übereinstimmung basierend auf ", "POSSIBLE_MATCH": "Mögliche Übereinstimmung", diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e2942a0460..8654c6e0d3 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -509,6 +509,7 @@ "ROW": "Row", "APPLY_TO_ALL_ROWS_NOTE": "+ denotes change can apply to all rows for this column", "BULK_IMPORT_DELETE_TASK": "Delete Import Task", + "BULK_IMPORT_DELETE_TASK_IN_PROGRESS": "Deleting...", "BULK_IMPORT_DATA_UPLOADED": "Data Uploaded", "BULK_IMPORT_IMAGE_UPLOADED": "Image Uploaded", "SPREADSHEET_UPLOADED_TITLE": "Spreadsheet Uploaded: {fileName}", @@ -762,6 +763,7 @@ "INDIVIDUAL_SCORE": "Individual Score", "IMAGE_SCORE": "Image Score", "NUMBER_OF_RESULTS": "Number of results", + "NUMBER_OF_RESULTS_MAX_HINT": "(max {max})", "SELECT_A_PROJECT": "select a project", "MATCHED_BASED_ON": "Matched based on ", "POSSIBLE_MATCH": "Possible Match", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 78f88a12d2..bca4a0bc5e 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -511,6 +511,7 @@ "ROW": "Fila", "APPLY_TO_ALL_ROWS_NOTE": "+ denota que el cambio se puede aplicar a todas las filas de esta columna", "BULK_IMPORT_DELETE_TASK": "Eliminar tarea de importación masiva", + "BULK_IMPORT_DELETE_TASK_IN_PROGRESS": "Eliminando...", "BULK_IMPORT_DATA_UPLOADED": "Datos subidos", "BULK_IMPORT_IMAGE_UPLOADED": "Imagen subida", "SPREADSHEET_UPLOADED_TITLE": "Hoja de cálculo subida: {fileName}", @@ -764,6 +765,7 @@ "INDIVIDUAL_SCORE": "Puntuación Individual", "IMAGE_SCORE": "Puntuación de Imagen", "NUMBER_OF_RESULTS": "Número de resultados", + "NUMBER_OF_RESULTS_MAX_HINT": "(máx. {max})", "SELECT_A_PROJECT": "seleccionar un proyecto", "MATCHED_BASED_ON": "Coincidencia basada en ", "POSSIBLE_MATCH": "Posible Coincidencia", diff --git a/frontend/src/locale/fr.json b/frontend/src/locale/fr.json index f6cdb7970b..8adf5a6ab2 100644 --- a/frontend/src/locale/fr.json +++ b/frontend/src/locale/fr.json @@ -511,6 +511,7 @@ "ROW": "Ligne", "APPLY_TO_ALL_ROWS_NOTE": "+ indique que la modification peut s'appliquer à toutes les lignes de cette colonne", "BULK_IMPORT_DELETE_TASK": "Supprimer la tâche d'importation en masse", + "BULK_IMPORT_DELETE_TASK_IN_PROGRESS": "Suppression...", "BULK_IMPORT_DATA_UPLOADED": "Données téléchargées", "BULK_IMPORT_IMAGE_UPLOADED": "Image téléchargée", "SPREADSHEET_UPLOADED_TITLE": "Feuille de calcul téléchargée: {fileName}", @@ -764,6 +765,7 @@ "INDIVIDUAL_SCORE": "Score Individuel", "IMAGE_SCORE": "Score d'Image", "NUMBER_OF_RESULTS": "Nombre de résultats", + "NUMBER_OF_RESULTS_MAX_HINT": "(max {max})", "SELECT_A_PROJECT": "sélectionner un projet", "MATCHED_BASED_ON": "Correspondance basée sur ", "POSSIBLE_MATCH": "Correspondance Possible", diff --git a/frontend/src/locale/it.json b/frontend/src/locale/it.json index 9afee9807f..60a69d041b 100644 --- a/frontend/src/locale/it.json +++ b/frontend/src/locale/it.json @@ -511,6 +511,7 @@ "ROW": "Riga", "APPLY_TO_ALL_ROWS_NOTE": "+ indica che la modifica può essere applicata a tutte le righe di questa colonna", "BULK_IMPORT_DELETE_TASK": "Elimina attività di importazione", + "BULK_IMPORT_DELETE_TASK_IN_PROGRESS": "Eliminazione in corso...", "BULK_IMPORT_DATA_UPLOADED": "Dati caricati", "BULK_IMPORT_IMAGE_UPLOADED": "Immagine caricata", "SPREADSHEET_UPLOADED_TITLE": "Foglio di calcolo caricato: {fileName}", @@ -764,6 +765,7 @@ "INDIVIDUAL_SCORE": "Punteggio Individuale", "IMAGE_SCORE": "Punteggio Immagine", "NUMBER_OF_RESULTS": "Numero di risultati", + "NUMBER_OF_RESULTS_MAX_HINT": "(max {max})", "SELECT_A_PROJECT": "seleziona un progetto", "MATCHED_BASED_ON": "Corrispondenza basata su ", "POSSIBLE_MATCH": "Possibile Corrispondenza", diff --git a/frontend/src/pages/BulkImport/BulkImportTask.jsx b/frontend/src/pages/BulkImport/BulkImportTask.jsx index 5ccab73160..6c7fecc0b5 100644 --- a/frontend/src/pages/BulkImport/BulkImportTask.jsx +++ b/frontend/src/pages/BulkImport/BulkImportTask.jsx @@ -35,7 +35,9 @@ const BulkImportTask = observer(() => { const [userRoles, setUserRoles] = useState(null); const store = useLocalObservable(() => new BulkImportTaskStore()); const [rowsPerPage, setRowsPerPage] = useState(10); - + const [isDeleting, setIsDeleting] = useState(false); + const [isSendingToIdentification, setIsSendingToIdentification] = + useState(false); const previousLocationID = task?.matchingLocations || []; const fetchData = async () => { @@ -76,12 +78,14 @@ const BulkImportTask = observer(() => { const deleteTask = async () => { if (!task?.id) return; + if (isDeleting) return; const confirmed = window.confirm( intl.formatMessage({ id: "BULK_IMPORT_DELETE_TASK_CONFIRM" }), ); if (!confirmed) return; + setIsDeleting(true); try { const res = await fetch(`/api/v3/bulk-import/${task.id}`, { method: "DELETE", @@ -102,6 +106,7 @@ const BulkImportTask = observer(() => { { error: err.message || "" }, ), ); + setIsDeleting(false); } }; @@ -486,50 +491,50 @@ const BulkImportTask = observer(() => { { + onClick={async () => { setShowError(false); - axios - .get( + setIsSendingToIdentification(true); + + try { + const response = await axios.get( `/appadmin/resendBulkImportID.jsp?importIdTask=${taskId}${store.locationIDString}`, - ) - .then((response) => { - if (response.status === 200) { - alert( - intl.formatMessage({ - id: "BULK_IMPORT_RE_ID_SUCCESS", - defaultMessage: - "Re-identification task started successfully.", - }), - ); - window.location.reload(); - } else { - throw new Error( - intl.formatMessage({ - id: "BULK_IMPORT_RE_ID_ERROR", - defaultMessage: - "Failed to start re-identification task.", - }), - ); - } - }) - .catch((error) => { - console.error( - "Error starting re-identification task:", - error, - ); + ); + + if (response.status === 200) { alert( + intl.formatMessage({ + id: "BULK_IMPORT_RE_ID_SUCCESS", + defaultMessage: + "Re-identification task started successfully.", + }), + ); + window.location.reload(); + } else { + throw new Error( intl.formatMessage({ id: "BULK_IMPORT_RE_ID_ERROR", defaultMessage: "Failed to start re-identification task.", }), ); - }); + } + } catch (error) { + console.error("Error starting re-identification task:", error); + alert( + intl.formatMessage({ + id: "BULK_IMPORT_RE_ID_ERROR", + defaultMessage: "Failed to start re-identification task.", + }), + ); + } finally { + setIsSendingToIdentification(false); + } }} backgroundColor={theme.wildMeColors.cyan700} color={theme.defaultColors.white} @@ -541,6 +546,15 @@ const BulkImportTask = observer(() => { marginLeft: 0, }} > + {isSendingToIdentification && ( + {((!userRoles?.includes("admin") && @@ -568,6 +582,7 @@ const BulkImportTask = observer(() => { { marginLeft: 0, marginTop: "1rem", marginBottom: "2rem", + opacity: isDeleting ? 0.7 : 1, + cursor: isDeleting ? "not-allowed" : "pointer", }} > - + {isDeleting ? ( + <> + + + + ) : ( + + )} diff --git a/frontend/src/pages/MatchResultsPage/MatchResults.jsx b/frontend/src/pages/MatchResultsPage/MatchResults.jsx index 2f5eaaf317..34bbf78da2 100644 --- a/frontend/src/pages/MatchResultsPage/MatchResults.jsx +++ b/frontend/src/pages/MatchResultsPage/MatchResults.jsx @@ -15,6 +15,7 @@ import FilterIcon from "./icons/FilterIcon"; import MatchCriteriaDrawer from "./components/MatchCriteriaDrawer"; import MultiSelectWithCheckbox from "../../components/MultiSelectWithCheckbox"; import ContainerWithSpinner from "../../components/ContainerWithSpinner"; +import { MAX_NUM_RESULTS } from "./constants"; const MatchResults = observer(() => { const themeColor = React.useContext(ThemeColorContext); @@ -234,6 +235,16 @@ const MatchResults = observer(() => { > + + +
- axios.patch( - `/api/v3/encounters/${encodeURIComponent(id)}`, - patchOps, - { + axios + .patch(`/api/v3/encounters/${encodeURIComponent(id)}`, patchOps, { headers: { "Content-Type": "application/json-patch+json", Accept: "application/json", }, - }, - ).then( - (response) => ({ status: "fulfilled", encounterId: id, response }), - (error) => ({ status: "rejected", encounterId: id, error }), - ), + }) + .then( + (response) => ({ status: "fulfilled", encounterId: id, response }), + (error) => ({ status: "rejected", encounterId: id, error }), + ), ); const results = await Promise.allSettled(patchPromises); @@ -601,14 +614,17 @@ export default class MatchResultsStore { ok: false, error: "CREATE_NEW_INDIVIDUAL_PARTIAL", successes, - failures: failures.map((f) => ({ encounterId: f.encounterId, error: f.error?.message || String(f.error) })), + failures: failures.map((f) => ({ + encounterId: f.encounterId, + error: f.error?.message || String(f.error), + })), }; } this.resetSelectionToQuery(); toast.success("New individual created successfully!"); return { ok: true, successes }; - } catch (e) { + } catch { this._matchRequestError = "CREATE_NEW_INDIVIDUAL_FAILED"; toast.error("Failed to create new individual"); return { ok: false, error: "CREATE_NEW_INDIVIDUAL_FAILED" }; @@ -702,7 +718,7 @@ export default class MatchResultsStore { this.resetSelectionToQuery(); toast.success("Match confirmed successfully!"); return res.data; - } catch (e) { + } catch { this._matchRequestError = "MATCH_FAILED"; toast.error("Failed to confirm match"); return null; @@ -751,7 +767,7 @@ export default class MatchResultsStore { this.resetSelectionToQuery(); toast.success("Merge page opened successfully!"); return { ok: true }; - } catch (e) { + } catch { this._matchRequestError = "MERGE_FAILED"; toast.error("Failed to start merge"); return null; diff --git a/src/main/java/org/ecocean/Annotation.java b/src/main/java/org/ecocean/Annotation.java index 577eeaec07..5ed14c612f 100644 --- a/src/main/java/org/ecocean/Annotation.java +++ b/src/main/java/org/ecocean/Annotation.java @@ -13,6 +13,8 @@ import org.ecocean.api.ApiException; import org.ecocean.ia.IA; import org.ecocean.ia.IAException; +import org.ecocean.ia.MatchResult; +import org.ecocean.ia.MatchResultProspect; import org.ecocean.ia.MLService; import org.ecocean.ia.Task; import org.ecocean.identity.IBEISIA; @@ -1598,7 +1600,6 @@ public static Base createFromApi(JSONObject payload, List files, Shepherd foundTrivial + " (and Feature) from " + ma + " and " + enc); } } - // we queue for embedding extraction so this is done in the background // and frees up foreground api process to return results to user // TODO myShepherd commit doesnt happen until we return; potential race condition on IA queue? @@ -1606,7 +1607,8 @@ public static Base createFromApi(JSONObject payload, List files, Shepherd task.addObject(ann); task.setStatusDetailsAddLog("Annotation.createFromApi() embedding extraction on " + ann); myShepherd.getPM().makePersistent(task); - System.out.println("[INFO] Annotation.createFromApi(): queueing for embedding extraction with " + task); + System.out.println( + "[INFO] Annotation.createFromApi(): queueing for embedding extraction with " + task); ann.queueForEmbeddingExtraction(task, myShepherd); return ann; } @@ -1736,6 +1738,59 @@ public int detachFromTasks(Shepherd myShepherd) { return tasks.size(); } + // we cant just detach the annots from match results, so we need + // to kill them off before we can delete an Annotation + public long deleteMatchResults(Shepherd myShepherd) { + return myShepherd.deleteMatchResults(this); + } + + // similar as above for MatchResultProspects + public int deleteMatchResultProspects(Shepherd myShepherd) { + List mrps = myShepherd.getMatchResultProspects(this); + int ct = 0; + + for (MatchResultProspect mrp : mrps) { + ct++; + System.out.println("[DEBUG] (" + ct + ") ann.deleteMatchResultProspects() on id=" + + this.getId() + " deleting " + mrp); + myShepherd.getPM().deletePersistent(mrp); + } + return ct; + } + + // when we delete an Annotation, we usually dont want to leave the Embeddings around + public int deleteEmbeddings(Shepherd myShepherd) { + int rtn = numberEmbeddings(); + + if (rtn < 1) return 0; + for (Embedding emb : embeddings) { + System.out.println("[DEBUG] ann.deleteEmbeddings() on id=" + this.getId() + + " deleting " + emb); + myShepherd.getPM().deletePersistent(emb); + } + return rtn; + } + + // a convenient method which does a typical set of steps to ready Annotation for deletion from db + // if encounter is already known, it can be passed (null will be ignored) + public void prepareForDeletion(Shepherd myShepherd, Encounter enc) { + int nt = this.detachFromTasks(myShepherd); + long t = System.currentTimeMillis(); + + if (enc != null) enc.removeAnnotation(this); + this.detachFromMediaAsset(); + long nm = this.deleteMatchResults(myShepherd); + int np = this.deleteMatchResultProspects(myShepherd); + int ne = this.deleteEmbeddings(myShepherd); + System.out.println("[INFO] ann.prepareForDeletion() [" + (System.currentTimeMillis() - t) + "ms]: " + nt + " Tasks, " + nm + + " MatchResults, " + np + " MatchResultProspects, " + ne + " Embeddings on " + this); + } + + // this version takes no enc, but will attempt to find it + public void prepareForDeletion(Shepherd myShepherd) { + prepareForDeletion(myShepherd, this.findEncounter(myShepherd)); + } + public static boolean isValidViewpoint(String vp) { if (vp == null) return true; return getAllValidViewpoints().contains(vp); diff --git a/src/main/java/org/ecocean/api/SiteSettings.java b/src/main/java/org/ecocean/api/SiteSettings.java index aa03a42ee2..3af244e14d 100644 --- a/src/main/java/org/ecocean/api/SiteSettings.java +++ b/src/main/java/org/ecocean/api/SiteSettings.java @@ -132,7 +132,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) for (JSONObject idOpt : iaConfig.identOpts(tx, iaClass)) { // make a copy so we can safely modify it JSONObject idOptCopy = new JSONObject(idOpt.toString()); - idOptCopy.remove("api_endpoint"); // dont want this shown + // FIXME we need to leave this endpoint in on site-settings for now + // as it is needed to kick off the matching from the client + // idOptCopy.remove("api_endpoint"); // NOTE: JSONObject.toString() in theory might produce different strings // for the same object (key ordering different); but in practice seems to // be consistent within these iterations diff --git a/src/main/java/org/ecocean/api/patch/EncounterPatchValidator.java b/src/main/java/org/ecocean/api/patch/EncounterPatchValidator.java index 540226db18..3336192cbd 100644 --- a/src/main/java/org/ecocean/api/patch/EncounterPatchValidator.java +++ b/src/main/java/org/ecocean/api/patch/EncounterPatchValidator.java @@ -234,9 +234,7 @@ public static JSONObject applyPatch(Encounter enc, JSONObject patch, User user, throw new ApiException("no such annotation id=" + value.toString(), ApiException.ERROR_RETURN_CODE_INVALID); MediaAsset ma = ann.getMediaAsset(); - ann.detachFromTasks(myShepherd); - enc.removeAnnotation(ann); - ann.detachFromMediaAsset(); + ann.prepareForDeletion(myShepherd, enc); // "most likely" this encounter is now detached from the asset, but we want them still connected // TODO parts might be connecting these, but how do we determine if we still need to add the trivial? if (ma != null) { @@ -345,8 +343,7 @@ private static JSONObject testJsonValue(Object value, String[] validFields) } // should never get called here with null value - private static MarkedIndividual getOrCreateMarkedIndividual(Object value, - Shepherd myShepherd) + private static MarkedIndividual getOrCreateMarkedIndividual(Object value, Shepherd myShepherd) throws ApiException { String idOrName = null; MarkedIndividual indiv = null; @@ -356,7 +353,6 @@ private static MarkedIndividual getOrCreateMarkedIndividual(Object value, // but we ignore that for now :) related, we dont check if this individual // exists with getMarkedIndividual() first JSONObject nameData = (JSONObject)value; - String type = nameData.optString("type", "NO_TYPE_GIVEN"); // right now we only support type=locationId, but may expand later if (type.equals("locationId")) { @@ -365,16 +361,17 @@ private static MarkedIndividual getOrCreateMarkedIndividual(Object value, idOrName = MarkedIndividual.nextNameByLocationId(locationId); } catch (IllegalArgumentException ex) { // can fail for various reasons like invalid locationId or one without a prefix - throw new ApiException("could not get next individual name for locationId (" + locationId + "): " + ex.getMessage(), - ApiException.ERROR_RETURN_CODE_INVALID); + throw new ApiException("could not get next individual name for locationId (" + + locationId + "): " + ex.getMessage(), + ApiException.ERROR_RETURN_CODE_INVALID); } } else { throw new ApiException("invalid type passed for new individual creation: " + type, - ApiException.ERROR_RETURN_CODE_INVALID); + ApiException.ERROR_RETURN_CODE_INVALID); } // if we fall through to here we should have idOrName to create a new one - System.out.println("[DEBUG] getOrCreateMarkedIndividual() creating '" + idOrName + "' based on " + nameData); - + System.out.println("[DEBUG] getOrCreateMarkedIndividual() creating '" + idOrName + + "' based on " + nameData); } else { // not json, so must have a name to find/create idOrName = value.toString(); indiv = myShepherd.getMarkedIndividual(idOrName); diff --git a/src/main/java/org/ecocean/ia/MatchResult.java b/src/main/java/org/ecocean/ia/MatchResult.java index c3ca6897ed..ebaec986f9 100644 --- a/src/main/java/org/ecocean/ia/MatchResult.java +++ b/src/main/java/org/ecocean/ia/MatchResult.java @@ -38,12 +38,18 @@ public class MatchResult implements java.io.Serializable { private Set prospects; private Annotation queryAnnotation; private int numberCandidates = 0; + // we store *actual* count here, but they may not all exist + // via .prospects due to MAXIMUM_PROSPECTS_STORED (see below) + private int numberProspects = 0; // not sure we really *need* true fk link to these annots // they might be gone now and will we ever use this? // so for now we just populate numberCandidates private Set candidates; // fallback number to cutoff number of prospects to return public static final int DEFAULT_PROSPECTS_CUTOFF = 100; + // number of MatchResultProspects [per type] to actually store (hotspotter + // results can produce thousands, but storing them all is excessive) + public static final int MAXIMUM_PROSPECTS_STORED = 500; public MatchResult() { id = Util.generateUUID(); @@ -159,6 +165,7 @@ private int populateProspects(String type, JSONArray annotIds, JSONArray scores, if (this.prospects == null) this.prospects = new HashSet(); int num = 0; + this.numberProspects += annotIds.length(); // true number of prospects for (int i = 0; i < annotIds.length(); i++) { double score = scores.optDouble(i, -Double.MAX_VALUE); String id = IBEISIA.fromFancyUUID(annotIds.optJSONObject(i)); @@ -174,11 +181,18 @@ private int populateProspects(String type, JSONArray annotIds, JSONArray scores, ma = createInspectionHeatmapAsset(externRef, id, myShepherd); this.prospects.add(new MatchResultProspect(ann, score, type, ma)); num++; + if (num >= MAXIMUM_PROSPECTS_STORED) { + System.out.println("[DEBUG] hit max (" + MAXIMUM_PROSPECTS_STORED + + ") number storable prospects on " + this); + break; + } } return num; } // we just have a list of annots which matched (e.g. via vectors in opensearch) + // NOTE: currently does not check MAXIMUM_PROSPECTS_STORED because vector search + // tends to return relatively few prospects. TODO adjust later if this proves untrue. private int populateProspects(List annots, boolean scoreByIndividual, Shepherd myShepherd) throws IOException { @@ -192,10 +206,12 @@ private int populateProspects(List annots, boolean scoreByIndividual // these scores are direct from opensearch for (Annotation ann : annots) { MediaAsset ma = createInspectionPairxAsset(this.queryAnnotation, ann, myShepherd); - this.prospects.add(new MatchResultProspect(ann, ann.getOpensearchScore(), "annot", ma)); + this.prospects.add(new MatchResultProspect(ann, ann.getOpensearchScore(), "annot", + ma)); } } - return this.prospects.size(); + this.numberProspects = this.prospects.size(); + return this.numberProspects; } private void _populateProspectsByIndividual(List annots, Shepherd myShepherd) { @@ -394,7 +410,7 @@ public int numberCandidates() { } */ public int numberProspects() { - return Util.collectionSize(prospects); + return this.numberProspects; } public Set prospectScoreTypes() { diff --git a/src/main/java/org/ecocean/ia/MatchResultProspect.java b/src/main/java/org/ecocean/ia/MatchResultProspect.java index a10bee63c4..32f6b1b71c 100644 --- a/src/main/java/org/ecocean/ia/MatchResultProspect.java +++ b/src/main/java/org/ecocean/ia/MatchResultProspect.java @@ -57,7 +57,7 @@ public boolean isInProjects(Set projectIds, Shepherd myShepherd) { } public String toString() { - return scoreType + ": " + score + " on " + annotation; + return scoreType + "=" + score + " on " + annotation + " for " + matchResult; } public JSONObject jsonForApiGet(Shepherd myShepherd) { diff --git a/src/main/java/org/ecocean/servlet/AnnotationEdit.java b/src/main/java/org/ecocean/servlet/AnnotationEdit.java index 24e327ca8e..d0cacb4f3b 100644 --- a/src/main/java/org/ecocean/servlet/AnnotationEdit.java +++ b/src/main/java/org/ecocean/servlet/AnnotationEdit.java @@ -42,7 +42,6 @@ public class AnnotationEdit extends HttpServlet { myShepherd.beginDBTransaction(); JSONObject jsonIn = ServletUtilities.jsonFromHttpServletRequest(request); PrintWriter out = response.getWriter(); - User user = AccessControl.getUser(request, myShepherd); boolean isAdmin = false; if (user != null) @@ -163,6 +162,9 @@ public class AnnotationEdit extends HttpServlet { myShepherd.getPM().deletePersistent(enc); rtn.put("encounterDeleted", true); } + annot.deleteMatchResults(myShepherd); + annot.deleteMatchResultProspects(myShepherd); + annot.deleteEmbeddings(myShepherd); myShepherd.getPM().deletePersistent(annot); myShepherd.getPM().deletePersistent(feat); System.out.println( diff --git a/src/main/java/org/ecocean/servlet/EncounterDelete.java b/src/main/java/org/ecocean/servlet/EncounterDelete.java index 8fd28a3959..02309c2123 100644 --- a/src/main/java/org/ecocean/servlet/EncounterDelete.java +++ b/src/main/java/org/ecocean/servlet/EncounterDelete.java @@ -20,9 +20,9 @@ // import java.util.Vector; import java.util.concurrent.ThreadPoolExecutor; -import org.ecocean.shepherd.core.Shepherd; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.ecocean.shepherd.core.Shepherd; public class EncounterDelete extends HttpServlet { private static final Logger log = LogManager.getLogger(EncounterDelete.class); @@ -162,15 +162,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) ArrayList anns = enc2trash.getAnnotations(); for (Annotation ann : anns) { myShepherd.beginDBTransaction(); - enc2trash.removeAnnotation(ann); - myShepherd.updateDBTransaction(); - List iaTasks = Task.getTasksFor(ann, myShepherd); - if (iaTasks != null && !iaTasks.isEmpty()) { - for (Task iaTask : iaTasks) { - iaTask.removeObject(ann); - myShepherd.updateDBTransaction(); - } - } + ann.prepareForDeletion(myShepherd, enc2trash); myShepherd.throwAwayAnnotation(ann); myShepherd.commitDBTransaction(); } diff --git a/src/main/java/org/ecocean/servlet/EncounterRemoveAnnotation.java b/src/main/java/org/ecocean/servlet/EncounterRemoveAnnotation.java index 8841fe0e4e..57518f4102 100644 --- a/src/main/java/org/ecocean/servlet/EncounterRemoveAnnotation.java +++ b/src/main/java/org/ecocean/servlet/EncounterRemoveAnnotation.java @@ -15,8 +15,8 @@ import java.io.*; import java.util.List; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; public class EncounterRemoveAnnotation extends HttpServlet { private static final Logger log = LogManager.getLogger(EncounterRemoveAnnotation.class); @@ -104,6 +104,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) enc.addComments("

Annotation deleted by " + user.getDisplayName() + " on " + Util.prettyTimeStamp() + "

"); + ann.deleteMatchResults(myShepherd); + ann.deleteMatchResultProspects(myShepherd); + ann.deleteEmbeddings(myShepherd); myShepherd.getPM().deletePersistent(ann); myShepherd.updateDBTransaction(); res.put("revertToTrivial", true); @@ -121,6 +124,9 @@ else if (!ann.isTrivial()) { "\">Annotation deleted by " + user.getDisplayName() + " on " + Util.prettyTimeStamp() + "

"); enc.removeAnnotation(ann); + ann.deleteMatchResults(myShepherd); + ann.deleteMatchResultProspects(myShepherd); + ann.deleteEmbeddings(myShepherd); myShepherd.getPM().deletePersistent(ann); myShepherd.commitDBTransaction(); } diff --git a/src/main/java/org/ecocean/servlet/importer/DeleteImportTask.java b/src/main/java/org/ecocean/servlet/importer/DeleteImportTask.java index a8e35c819e..56a6950e08 100644 --- a/src/main/java/org/ecocean/servlet/importer/DeleteImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/DeleteImportTask.java @@ -9,8 +9,8 @@ import org.ecocean.Project; import org.ecocean.security.Collaboration; import org.ecocean.servlet.ServletUtilities; -import org.ecocean.social.SocialUnit; import org.ecocean.shepherd.core.Shepherd; +import org.ecocean.social.SocialUnit; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -62,15 +62,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) List projects = myShepherd.getProjectsForEncounter(enc); ArrayList anns = enc.getAnnotations(); for (Annotation ann : anns) { - enc.removeAnnotation(ann); - myShepherd.updateDBTransaction(); - List iaTasks = Task.getTasksFor(ann, myShepherd); - if (iaTasks != null && !iaTasks.isEmpty()) { - for (Task iaTask : iaTasks) { - iaTask.removeObject(ann); - myShepherd.updateDBTransaction(); - } - } + ann.prepareForDeletion(myShepherd, enc); myShepherd.throwAwayAnnotation(ann); myShepherd.updateDBTransaction(); } diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index 73906e0b10..e8ad049079 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -403,7 +403,8 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { // this records only most recent task statuses like: numLatestTask_complete if (latestTask) { String latestStatus = "numLatestTask_" + atask.getStatus(myShepherd); - System.out.println("[DEBUG] (ImportTask " + this.getId() + ") latestStatus for Task " + atask.getId() + ": " + latestStatus); + System.out.println("[DEBUG] (ImportTask " + this.getId() + + ") latestStatus for Task " + atask.getId() + ": " + latestStatus); if (sa.has(latestStatus)) { sa.put(latestStatus, sa.optInt(latestStatus, 0) + 1); } else { @@ -509,17 +510,8 @@ public static void deleteWithRelated(String id, User user, Shepherd myShepherd) List projects = myShepherd.getProjectsForEncounter(enc); ArrayList anns = enc.getAnnotations(); for (Annotation ann : anns) { - enc.removeAnnotation(ann); - // myShepherd.updateDBTransaction(); - List iaTasks = Task.getTasksFor(ann, myShepherd); - if (iaTasks != null && !iaTasks.isEmpty()) { - for (Task iaTask : iaTasks) { - iaTask.removeObject(ann); - // myShepherd.updateDBTransaction(); - } - } + ann.prepareForDeletion(myShepherd, enc); myShepherd.throwAwayAnnotation(ann); - // myShepherd.updateDBTransaction(); } // handle occurrences if (occ != null) { @@ -638,7 +630,9 @@ public JSONObject iaSummaryJson(Shepherd myShepherd) { pj.put("detectionPercent", 1.0); pj.put("detectionStatus", "complete"); } else { - if (numAssets > 0) pj.put("detectionPercent", new Double(numDetectionComplete) / new Double(numAssets)); + if (numAssets > 0) + pj.put("detectionPercent", + new Double(numDetectionComplete) / new Double(numAssets)); pj.put("detectionStatus", "sent"); } if (this.iaTaskRequestedIdentification()) { @@ -695,11 +689,12 @@ public JSONObject iaSummaryJson(Shepherd myShepherd) { static Map parseSqlCountResults(Query query) { Map map = new HashMap<>(); + try { List results = query.executeList(); for (Object row : results) { - Object[] cols = (Object[]) row; - map.put((String) cols[0], ((Number) cols[1]).intValue()); + Object[] cols = (Object[])row; + map.put((String)cols[0], ((Number)cols[1]).intValue()); } } catch (Exception e) { e.printStackTrace(); @@ -712,28 +707,28 @@ static Map parseSqlCountResults(Query query) { public static Map getAllEncounterCounts(Shepherd myShepherd) { Query query = myShepherd.getPM().newQuery("javax.jdo.query.SQL", "SELECT \"ID_OID\", count(*) FROM \"IMPORTTASK_ENCOUNTERS\" GROUP BY \"ID_OID\""); + return parseSqlCountResults(query); } public static Map getAllIndividualCounts(Shepherd myShepherd) { Query query = myShepherd.getPM().newQuery("javax.jdo.query.SQL", "SELECT ie.\"ID_OID\", count(distinct me.\"INDIVIDUALID_OID\") " + - "FROM \"IMPORTTASK_ENCOUNTERS\" ie " + - "JOIN \"MARKEDINDIVIDUAL_ENCOUNTERS\" me " + - "ON ie.\"CATALOGNUMBER_EID\" = me.\"CATALOGNUMBER_EID\" " + - "GROUP BY ie.\"ID_OID\""); + "FROM \"IMPORTTASK_ENCOUNTERS\" ie " + "JOIN \"MARKEDINDIVIDUAL_ENCOUNTERS\" me " + + "ON ie.\"CATALOGNUMBER_EID\" = me.\"CATALOGNUMBER_EID\" " + "GROUP BY ie.\"ID_OID\""); + return parseSqlCountResults(query); } public static Map getAllMediaAssetCounts(Shepherd myShepherd) { Query query = myShepherd.getPM().newQuery("javax.jdo.query.SQL", "SELECT ie.\"ID_OID\", count(distinct mf.\"ID_OID\") " + - "FROM \"IMPORTTASK_ENCOUNTERS\" ie " + - "JOIN \"ENCOUNTER_ANNOTATIONS\" ea " + + "FROM \"IMPORTTASK_ENCOUNTERS\" ie " + "JOIN \"ENCOUNTER_ANNOTATIONS\" ea " + "ON ie.\"CATALOGNUMBER_EID\" = ea.\"CATALOGNUMBER_OID\" " + "JOIN \"ANNOTATION_FEATURES\" af ON ea.\"ID_EID\" = af.\"ID_OID\" " + "JOIN \"MEDIAASSET_FEATURES\" mf ON af.\"ID_EID\" = mf.\"ID_EID\" " + "GROUP BY ie.\"ID_OID\""); + return parseSqlCountResults(query); } } diff --git a/src/main/java/org/ecocean/shepherd/core/Shepherd.java b/src/main/java/org/ecocean/shepherd/core/Shepherd.java index 52b5a1fef6..32a4c9582b 100644 --- a/src/main/java/org/ecocean/shepherd/core/Shepherd.java +++ b/src/main/java/org/ecocean/shepherd/core/Shepherd.java @@ -17,6 +17,7 @@ import org.ecocean.grid.ScanTask; import org.ecocean.grid.ScanWorkItem; import org.ecocean.ia.MatchResult; +import org.ecocean.ia.MatchResultProspect; import org.ecocean.ia.Task; import org.ecocean.media.*; import org.ecocean.movement.Path; @@ -2830,6 +2831,46 @@ public List getMatchResults(Task task) { return all; } + public List getMatchResults(Annotation ann) { + List all = new ArrayList(); + + if (ann == null) return all; + String filter = "SELECT FROM org.ecocean.ia.MatchResult WHERE queryAnnotation.id == '" + + ann.getId() + "'"; + Query query = pm.newQuery(filter); + query.setOrdering("created DESC"); + Collection c = (Collection)query.execute(); + if (c != null) all = new ArrayList(c); + query.closeAll(); + return all; + } + + // faster deletion of all MatchResults associated with Annotation + public long deleteMatchResults(Annotation ann) { + if (ann == null) return 0l; + long t = System.currentTimeMillis(); + String filter = "SELECT FROM org.ecocean.ia.MatchResult WHERE queryAnnotation.id == '" + + ann.getId() + "'"; + Query query = pm.newQuery(filter); + long ct = query.deletePersistentAll(); + query.closeAll(); + System.out.println("[DEBUG] deleteMatchResults() deleted " + ct + " [" + (System.currentTimeMillis() - t) + "ms] on " + ann); + return ct; + } + + public List getMatchResultProspects(Annotation ann) { + List all = new ArrayList(); + + if (ann == null) return all; + String filter = "SELECT FROM org.ecocean.ia.MatchResultProspect WHERE annotation.id == '" + + ann.getId() + "'"; + Query query = pm.newQuery(filter); + Collection c = (Collection)query.execute(); + if (c != null) all = new ArrayList(c); + query.closeAll(); + return all; + } + public MarkedIndividual getMarkedIndividualQuiet(String name) { MarkedIndividual indiv = null; diff --git a/src/main/resources/org/ecocean/ia/package.jdo b/src/main/resources/org/ecocean/ia/package.jdo index b47b49b153..0e69cd893f 100755 --- a/src/main/resources/org/ecocean/ia/package.jdo +++ b/src/main/resources/org/ecocean/ia/package.jdo @@ -86,7 +86,7 @@ alter table "TASK" alter column "PARAMETERS" type text; - +