diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java
index 7af55e9d..fac2ec7f 100644
--- a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java
+++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java
@@ -2,6 +2,7 @@
import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException;
import au.org.aodn.ogcapi.server.core.exception.UnauthorizedServerException;
+import au.org.aodn.ogcapi.server.core.model.enumeration.SseEventName;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -50,7 +51,7 @@ public static void handleError(Exception e, String uuid, SseEmitter emitter, Run
case VALIDATION_ERROR -> {
log.warn("Invalid parameter error for UUID {}: {}", uuid, e.getMessage());
emitter.send(SseEmitter.event()
- .name("error")
+ .name(SseEventName.ERROR.getValue())
.data(Map.of(
"message", "Invalid parameter error",
"timestamp", System.currentTimeMillis()
@@ -61,7 +62,7 @@ public static void handleError(Exception e, String uuid, SseEmitter emitter, Run
case UNAUTHORIZED_SERVER_ERROR -> {
log.warn("Unauthorized wfs server for UUID {}", uuid, e);
emitter.send(SseEmitter.event()
- .name("error")
+ .name(SseEventName.ERROR.getValue())
.data(Map.of(
"message", "Unauthorized wfs server",
"timestamp", System.currentTimeMillis()
@@ -72,7 +73,7 @@ public static void handleError(Exception e, String uuid, SseEmitter emitter, Run
case DOWNLOADABLE_FIELDS_ERROR -> {
log.warn("No downloadable fields found for UUID {}", uuid, e);
emitter.send(SseEmitter.event()
- .name("error")
+ .name(SseEventName.ERROR.getValue())
.data(Map.of(
"message", "No downloadable fields found",
"timestamp", System.currentTimeMillis()
@@ -83,7 +84,7 @@ public static void handleError(Exception e, String uuid, SseEmitter emitter, Run
case UNKNOWN_ERROR -> {
log.warn("Unknown error for UUID {}", uuid, e);
emitter.send(SseEmitter.event()
- .name("error")
+ .name(SseEventName.ERROR.getValue())
.data(Map.of(
"message", "Unknown error: " + e.getMessage(),
"timestamp", System.currentTimeMillis()
diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/ProcessIdEnum.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/ProcessIdEnum.java
index a7e3966b..48221df2 100644
--- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/ProcessIdEnum.java
+++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/ProcessIdEnum.java
@@ -7,6 +7,7 @@ public enum ProcessIdEnum {
DOWNLOAD_DATASET("download"),
DOWNLOAD_WFS_SSE("downloadWfs"),
DOWNLOAD_WFS_ESTIMATE("estimateWfsDownload"),
+ DOWNLOAD_CO_ESTIMATE("estimateCOdownload"),
UNKNOWN("");
private final String value;
diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/SseEventName.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/SseEventName.java
new file mode 100644
index 00000000..5879f17a
--- /dev/null
+++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/SseEventName.java
@@ -0,0 +1,28 @@
+package au.org.aodn.ogcapi.server.core.model.enumeration;
+
+import lombok.Getter;
+
+/**
+ * Names of the SSE events sent by the download / size-estimate streams.
+ *
+ * The frontend identifies events by these literal names, so they form a wire
+ * contract — change a value only together with the portal (and keep them
+ * aligned with the data-access-service sse_wrapper vocabulary).
+ */
+@Getter
+public enum SseEventName {
+ CONNECTION_ESTABLISHED("connection-established"),
+ KEEP_ALIVE("keep-alive"),
+ DOWNLOAD_STARTED("download-started"),
+ FILE_CHUNK("file-chunk"),
+ DOWNLOAD_COMPLETE("download-complete"),
+ ESTIMATE_COMPLETE("estimate-complete"),
+ ESTIMATE_FAILED("estimate-failed"),
+ ERROR("error");
+
+ private final String value;
+
+ SseEventName(String value) {
+ this.value = value;
+ }
+}
diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/DasService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/DasService.java
index bcc356a8..51a5d94c 100644
--- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/DasService.java
+++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/DasService.java
@@ -11,6 +11,7 @@
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
@Service("DataAccessService")
@@ -65,7 +66,7 @@ public byte[] getWaveBuoysLatestAvailableDate() {
.encode()
.toUriString();
- return httpClient.exchange(waveBuoysUrlTemplate, HttpMethod.GET,httpEntity,byte[].class).getBody();
+ return httpClient.exchange(waveBuoysUrlTemplate, HttpMethod.GET, httpEntity, byte[].class).getBody();
}
public byte[] getWaveBuoyDetailsBetweenDates(String startDateTime, String endDateTime, String buoy) {
@@ -88,6 +89,53 @@ public byte[] getMooringDetailsBetweenDates(String startDateTime, String endDate
return getFeatureCollection("/api/v1/das/data/feature-collection/mooring/{mooring}", startDateTime, endDateTime, Map.of("mooring", mooring));
}
+ /**
+ * Call the data-access-service cloud-optimised size estimate endpoint.
+ * Returns the raw JSON response body so the SSE layer can forward
+ * it to the frontend unchanged.
+ */
+ public String estimateCloudOptimisedDownloadSize(
+ String uuid,
+ List keys,
+ String startDate,
+ String endDate,
+ Object multiPolygon,
+ List columns,
+ String outputFormat) {
+
+ String url = UriComponentsBuilder.fromUriString(dasConfig.host() + "/api/v1/das/data/{uuid}/estimate_size")
+ .encode()
+ .toUriString();
+
+ Map body = new HashMap<>();
+ body.put("keys", keys);
+ body.put("start_date", startDate != null ? startDate : "non-specified");
+ body.put("end_date", endDate != null ? endDate : "non-specified");
+ body.put("output_format", outputFormat);
+ // multi_polygon is accepted as a GeoJSON object or string; forward as-is.
+ if (multiPolygon != null) {
+ body.put("multi_polygon", multiPolygon);
+ }
+ // columns is not sent today (frontend doesn't subset columns yet, and the
+ // batch download grabs all variables), keeping the estimate aligned.
+ if (columns != null) {
+ body.put("columns", columns);
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+ headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+ headers.set("X-API-KEY", dasConfig.secret());
+ headers.set("x-internal-das-header-secret", dasConfig.internal());
+
+ HttpEntity