Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 12 additions & 22 deletions frontend/src/pages/Encounter/Encounter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ import { Divider } from "antd";
import { get } from "lodash-es";
import CollabModal from "./CollabModal";
import Alert from "react-bootstrap/Alert";
import {
shouldContinuePollingEncounter,
POLL_INTERVAL_MS,
MAX_POLL_CYCLES,
} from "./pollingHelpers";

const Encounter = observer(() => {
const [store] = useState(() => new EncounterStore());
Expand Down Expand Up @@ -72,26 +77,7 @@ const Encounter = observer(() => {
useEffect(() => {
let cancelled = false;
let timeoutId = null;

const isTerminalDetectionStatus = (status) =>
!status ||
status === "complete" ||
status === "error" ||
status === "pending";

const shouldContinuePolling = (encounterData) => {
const mediaAssets = Array.isArray(encounterData?.mediaAssets)
? encounterData.mediaAssets
: [];

if (mediaAssets.length === 0) {
return false;
}

return mediaAssets.some(
(asset) => !isTerminalDetectionStatus(asset?.detectionStatus),
);
};
let pollCount = 0;

const fetchEncounter = async () => {
try {
Expand All @@ -108,8 +94,12 @@ const Encounter = observer(() => {
store.setMediaAssets(res.data.mediaAssets);
}

if (shouldContinuePolling(res.data)) {
timeoutId = window.setTimeout(fetchEncounter, 3000);
pollCount++;
if (
pollCount < MAX_POLL_CYCLES &&
shouldContinuePollingEncounter(res.data)
) {
timeoutId = window.setTimeout(fetchEncounter, POLL_INTERVAL_MS);
}
} catch (_err) {
if (!cancelled && isInitialLoad.current) {
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/pages/Encounter/ImageCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Tooltip from "../../components/ToolTip";
import axios from "axios";
import { useIntl } from "react-intl";
import SpotMappingIcon2 from "../../components/icons/SpotMappingIcon2";
import { isAssetActivelyAwaitingDetection } from "./pollingHelpers";

const ImageCard = observer(({ store = {} }) => {
const imgRef = useRef(null);
Expand Down Expand Up @@ -67,17 +68,11 @@ const ImageCard = observer(({ store = {} }) => {
JSON.stringify(editAnnotationParams),
);

const isTerminalDetectionStatus = (status) =>
!status ||
status === "complete" ||
status === "error" ||
status === "pending";

const selectedAsset =
store.encounterData?.mediaAssets?.[store.selectedImageIndex];
const selectedAssetDetectionStatus = selectedAsset?.detectionStatus;
const isDetectionInProgress =
!!selectedAsset && !isTerminalDetectionStatus(selectedAssetDetectionStatus);
!!selectedAsset &&
isAssetActivelyAwaitingDetection(selectedAsset, store.encounterData);

const handleEnter = (text) => setTip((s) => ({ ...s, show: true, text }));
const handleMove = (e) => {
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/pages/Encounter/pollingHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Shared detection-status predicates used by Encounter.jsx polling loop and
// ImageCard.jsx "Detection in progress" indicator. Keeping them here avoids
// the two files drifting out of sync.

const POLL_INTERVAL_MS = 3000;
const MAX_POLL_CYCLES = 100; // 100 cycles * 3s = ~5 minutes

const isAnnotationTrivial = (a) => a?.isTrivial === true || a?.trivial === true;

export const isTerminalDetectionStatus = (status) =>
!status ||
status === "complete" ||
status === "error" ||
status === "pending";

// True when an asset is from a bulk import and detection has been queued
// to WBIA but the callback hasn't returned yet. In this state the API
// returns detectionStatus=null and only a trivial placeholder annotation.
// Scoped to encounters that have an importTaskId so we don't poll forever
// on legacy/manual encounters that intentionally never run detection.
export const isAwaitingBulkImportDetection = (asset, encounterData) => {
if (!encounterData?.importTaskId) return false;
if (asset?.detectionStatus !== null && asset?.detectionStatus !== undefined)
return false;
const anns = Array.isArray(asset?.annotations) ? asset.annotations : [];
return anns.length > 0 && anns.every(isAnnotationTrivial);
};

export const isAssetActivelyAwaitingDetection = (asset, encounterData) =>
!isTerminalDetectionStatus(asset?.detectionStatus) ||
isAwaitingBulkImportDetection(asset, encounterData);

export const shouldContinuePollingEncounter = (encounterData) => {
const mediaAssets = Array.isArray(encounterData?.mediaAssets)
? encounterData.mediaAssets
: [];
if (mediaAssets.length === 0) return false;
return mediaAssets.some((asset) =>
isAssetActivelyAwaitingDetection(asset, encounterData),
);
};

export { POLL_INTERVAL_MS, MAX_POLL_CYCLES };
2 changes: 1 addition & 1 deletion frontend/src/pages/Encounter/stores/EncounterStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class EncounterStore {

setMediaAssets(mediaAssets) {
if (this._encounterData) {
this._encounterData.mediaAssets = mediaAssets;
this._encounterData = { ...this._encounterData, mediaAssets };
}
}

Expand Down
5 changes: 3 additions & 2 deletions frontend/src/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ registerRoute(
}),
);

// Never cache API endpoints — responses are not idempotent and stale data
// (e.g. boundingBox, detectionStatus) breaks polling-driven UI updates.
registerRoute(
// ({url}) => true,
() => true,
({ url }) => !url.pathname.startsWith("/api/"),
new NetworkFirst(),
);

Expand Down
31 changes: 18 additions & 13 deletions src/main/java/org/ecocean/media/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class Feature implements java.io.Serializable {
protected FeatureType type;

protected JSONObject parameters;
protected String parametersAsString;

// this link back to the objs with .features that include us
protected Annotation annotation;
Expand Down Expand Up @@ -52,6 +53,7 @@ public Feature(final String id, final FeatureType type, final JSONObject params)
this.id = id;
this.type = type;
this.parameters = params;
if (params != null) this.parametersAsString = params.toString();
this.setRevision();
}

Expand Down Expand Up @@ -90,31 +92,34 @@ public MediaAsset getMediaAsset() {
}

public JSONObject getParameters() {
// System.out.println("getParameters() called -> " + parameters);
if (parameters != null) return parameters;
if (parametersAsString == null) return null;
try {
parameters = new JSONObject(parametersAsString);
} catch (JSONException je) {
System.out.println(this + " -- error parsing parameters json string (" +
parametersAsString + "): " + je.toString());
return null;
}
return parameters;
}

public void setParameters(JSONObject p) {
// System.out.println("setParameters(" + p + ") called");
parameters = p;
parametersAsString = (p == null) ? null : p.toString();
}

// only DataNucleus should be calling get/setParametersAsString. always use get/setParameters() instead.
public String getParametersAsString() {
// System.out.println("getParametersAsString() called -> " + parameters);
if (parametersAsString != null) return parametersAsString;
if (parameters == null) return null;
return parameters.toString();
parametersAsString = parameters.toString();
return parametersAsString;
}

public void setParametersAsString(String p) {
// System.out.println("setParametersAsString(" + p + ") called");
if (p == null) return;
try {
parameters = new JSONObject(p);
} catch (JSONException je) {
System.out.println(this + " -- error parsing parameters json string (" + p + "): " +
je.toString());
parameters = null;
}
parametersAsString = p;
parameters = null; // force lazy re-parse on next getParameters()
}

public long getRevision() {
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/org/ecocean/media/package.jdo
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@
<field name="revision" persistence-modifier="persistent">
<column jdbc-type="BIGINT" allows-null="false"/>
</field>
<property name="parametersAsString" persistence-modifier="persistent">
<field name="parametersAsString" persistence-modifier="persistent">
<column jdbc-type="LONGVARCHAR" name="parameters" />
</property>
</field>

<field name="annotation" />
<field name="asset" />
Expand Down
Loading