diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java index a4e4eb34..546c8fba 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java @@ -17,6 +17,9 @@ public enum Parameter { TYPE("type"), FIELDS("fields"), LAYER_NAME("layer_name"), + COLLECTION_TITLE("collection_title"), + FULL_METADATA_LINK("full_metadata_link"), + SUGGESTED_CITATION("suggested_citation"), ; private final String value; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java index 96b61472..4386410f 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java @@ -34,22 +34,80 @@ public static String readBase64Image(String filename) throws IOException { } /** - * Generate HTML content for bounding box section in email + * Generate the complete subsetting section HTML (header + bbox + time range) + * Returns empty string if no subsetting is applied + */ + public static String generateSubsettingSection( + String startDate, + String endDate, + Object multipolygon, + ObjectMapper objectMapper + ) { + try { + // Format dates + String displayStartDate = (startDate != null && !startDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) + ? startDate.replace("-", "/") : ""; + String displayEndDate = (endDate != null && !endDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) + ? endDate.replace("-", "/") : ""; + + // Check if dates are specified + boolean hasDateSubsetting = !displayStartDate.isEmpty() || !displayEndDate.isEmpty(); + + // Check if bbox is specified + boolean hasBboxSubsetting = multipolygon != null && !isEmptyMultiPolygon(multipolygon); + + // If no subsetting at all, return empty string + if (!hasDateSubsetting && !hasBboxSubsetting) { + return ""; + } + + StringBuilder html = new StringBuilder(); + + // Add subsetting header + if (hasBboxSubsetting || hasDateSubsetting) { + html.append(buildSubsettingHeader()); + } + + // Add bbox section if present + if (hasBboxSubsetting) { + html.append(buildBboxWrapper(generateBboxHtml(multipolygon, objectMapper))); + } + + // Add spacing between bbox and time range if both exist + if (hasBboxSubsetting && hasDateSubsetting) { + html.append(buildSpacerSection()); + } + + // Add time range section if present + if (hasDateSubsetting) { + html.append(buildTimeRangeWrapper(displayStartDate, displayEndDate)); + } + + return html.toString(); + + } catch (Exception e) { + log.error("Error generating subsetting section", e); + return ""; + } + } + + /** + * Generate HTML content for bounding box data only (without wrapper) * @param multipolygon - the multipolygon object * @param objectMapper - Jackson ObjectMapper for JSON processing - * @return HTML string for bbox section + * @return HTML string for bbox data rows */ public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMapper) { try { if (multipolygon == null) { - return buildBboxSection("0", "0", "0", "0", 0); + return ""; } // Extract coordinates directly from the object List>>> coordinates = extractCoordinates(multipolygon, objectMapper); if (coordinates == null || coordinates.isEmpty()) { - return buildBboxSection("0", "0", "0", "0", 0); + return ""; } StringBuilder html = new StringBuilder(); @@ -59,9 +117,9 @@ public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMa for (List>> polygon : coordinates) { // Find min/max for THIS polygon only double minLon = Double.MAX_VALUE; - double maxLon = Double.MIN_VALUE; + double maxLon = Double.NEGATIVE_INFINITY; double minLat = Double.MAX_VALUE; - double maxLat = Double.MIN_VALUE; + double maxLat = Double.NEGATIVE_INFINITY; for (List> ring : polygon) { for (List point : ring) { @@ -106,7 +164,103 @@ public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMa } catch (Exception e) { log.error("Error generating bbox HTML", e); - return buildBboxSection("0", "0", "0", "0", 0); + return ""; + } + } + + /** + * Check if multipolygon is empty or represents the full world + */ + private static boolean isEmptyMultiPolygon(Object multipolygon) { + try { + // Null check + if (multipolygon == null) { + return true; + } + + // Check for "non-specified" string + if (multipolygon instanceof String && "non-specified".equals(multipolygon.toString())) { + return true; + } + + if (multipolygon instanceof Map) { + Map map = (Map) multipolygon; + Object coords = map.get("coordinates"); + + // No coordinates field at all + if (coords == null) { + return true; + } + + if (coords instanceof List) { + List coordsList = (List) coords; + // Empty coordinates + if (coordsList.isEmpty()) { + return true; + } + // Full world bbox + if (isFullWorldBbox(coordsList)) { + return true; + } + // Has valid coordinates + return false; + } else { + // coordinates exists but is not a List + return true; + } + } + + // Not a Map, not a String, treat as empty + return true; + } catch (Exception e) { + log.warn("Error checking if multipolygon is empty, treating as empty", e); + return true; + } + } + + /** + * Check if coordinates represent the full world (±180 longitude, ±90 latitude) + */ + private static boolean isFullWorldBbox(List coordinates) { + try { + // Check each polygon in the MultiPolygon + for (Object polygonObj : coordinates) { + if (!(polygonObj instanceof List)) continue; + List polygon = (List) polygonObj; + + // Check each ring in the polygon + for (Object ringObj : polygon) { + if (!(ringObj instanceof List)) continue; + List ring = (List) ringObj; + + // Count how many world boundary points we find + boolean hasMaxLon = false; // 180 or -180 + boolean hasMaxLat = false; // 90 + boolean hasMinLat = false; // -90 + + for (Object pointObj : ring) { + if (!(pointObj instanceof List)) continue; + List point = (List) pointObj; + + if (point.size() >= 2) { + double lon = ((Number) point.get(0)).doubleValue(); + double lat = ((Number) point.get(1)).doubleValue(); + + if (Math.abs(lon) == 180.0) hasMaxLon = true; + if (lat == 90.0) hasMaxLat = true; + if (lat == -90.0) hasMinLat = true; + } + } + + // If we found all world boundaries, it's the full world + if (hasMaxLon && hasMaxLat && hasMinLat) { + return true; + } + } + } + return false; + } catch (Exception e) { + return false; } } @@ -135,6 +289,101 @@ private static List>>> extractCoordinates(Object mult return null; } + /** + * Build the subsetting section header + */ + private static String buildSubsettingHeader() { + return "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "
" + + "
" + + "
" + + "

Subsetting for this collection:

" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
"; + } + + /** + * Build bbox wrapper with table structure + */ + private static String buildBboxWrapper(String bboxContent) { + return "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "" + bboxContent + "
" + + "
"; + } + + /** + * Build spacer section between bbox and time range + */ + private static String buildSpacerSection() { + return "" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
"; + } + + /** + * Build time range wrapper with content + */ + private static String buildTimeRangeWrapper(String startDate, String endDate) { + return "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "
" + + "
" + + "
" + + "
" + + "
" + + "

Time Range

" + + "" + + "" + + "
" + + "
" + + "
" + + "

"+startDate+" - "+endDate+"

" + + "
"; + } + protected static String buildBboxSection(String north, String south, String west, String east, int index) { String title = index > 0 ? "Bounding Box " + index : "Bounding Box Selection"; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java index d17ee4a4..402f5951 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java @@ -56,11 +56,14 @@ public ResponseEntity execute( var endDate = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.END_DATE.getValue()); var multiPolygon = body.getInputs().get(DatasetDownloadEnums.Parameter.MULTI_POLYGON.getValue()); var recipient = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.RECIPIENT.getValue()); + var collectionTitle = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.COLLECTION_TITLE.getValue()); + var fullMetadataLink = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.FULL_METADATA_LINK.getValue()); + var suggestedCitation = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.SUGGESTED_CITATION.getValue()); // move the notify user email from data-access-service to here to make the first email faster - restServices.notifyUser(recipient, uuid, startDate, endDate, multiPolygon); + restServices.notifyUser(recipient, uuid, startDate, endDate, multiPolygon, collectionTitle, fullMetadataLink, suggestedCitation); - var response = restServices.downloadData(uuid, startDate, endDate, multiPolygon, recipient); + var response = restServices.downloadData(uuid, startDate, endDate, multiPolygon, recipient, collectionTitle, fullMetadataLink, suggestedCitation); var value = new InlineValue(response.getBody()); var status = new InlineValue(Integer.toString(HttpStatus.OK.value())); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java index 97cd0e1e..12a8c256 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java @@ -41,13 +41,15 @@ public RestServices(BatchClient batchClient, ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - public void notifyUser(String recipient, String uuid, String startDate, String endDate, Object multiPolygon) { + public void notifyUser(String recipient, String uuid, String startDate, String endDate, Object multiPolygon, String collectionTitle, + String fullMetadataLink, + String suggestedCitation) { String aodnInfoSender = "no.reply@aodn.org.au"; try (SesClient ses = SesClient.builder().build()) { var subject = Content.builder().data("Start processing data file whose uuid is: " + uuid).build(); - var content = Content.builder().data(generateStartedEmailContent(uuid, startDate, endDate, multiPolygon)).build(); + var content = Content.builder().data(generateStartedEmailContent(uuid, startDate, endDate, multiPolygon, collectionTitle, fullMetadataLink, suggestedCitation)).build(); var destination = Destination.builder().toAddresses(recipient).build(); var body = Body.builder().html(content).build(); @@ -74,7 +76,10 @@ public ResponseEntity downloadData( String startDate, String endDate, Object polygons, - String recipient + String recipient, + String collectionTitle, + String fullMetadataLink, + String suggestedCitation ) throws JsonProcessingException { Map parameters = new HashMap<>(); @@ -82,6 +87,9 @@ public ResponseEntity downloadData( parameters.put(DatasetDownloadEnums.Parameter.START_DATE.getValue(), startDate); parameters.put(DatasetDownloadEnums.Parameter.END_DATE.getValue(), endDate); parameters.put(DatasetDownloadEnums.Parameter.RECIPIENT.getValue(), recipient); + parameters.put(DatasetDownloadEnums.Parameter.COLLECTION_TITLE.getValue(), collectionTitle); + parameters.put(DatasetDownloadEnums.Parameter.FULL_METADATA_LINK.getValue(), fullMetadataLink); + parameters.put(DatasetDownloadEnums.Parameter.SUGGESTED_CITATION.getValue(), suggestedCitation); if (polygons == null || polygons.toString().isEmpty()) { throw new IllegalArgumentException("Polygons parameter should now be null. If users didn't specify polygons, a 'non-specified' should be sent."); @@ -119,7 +127,15 @@ private String submitJob(String jobName, String jobQueue, String jobDefinition, return submitJobResponse.jobId(); } - private String generateStartedEmailContent(String uuid, String startDate, String endDate, Object multipolygon) { + private String generateStartedEmailContent( + String uuid, + String startDate, + String endDate, + Object multipolygon, + String collectionTitle, + String fullMetadataLink, + String suggestedCitation + ) { try (InputStream inputStream = getClass().getResourceAsStream("/job-started-email.html")) { if (inputStream == null) { @@ -129,19 +145,21 @@ private String generateStartedEmailContent(String uuid, String startDate, String String template = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - // Handle dates - only show if not "non-specified" - String displayStartDate = (startDate != null && !startDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? startDate.replace("-", "/") : ""; - String displayEndDate = (endDate != null && !endDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? endDate.replace("-", "/") : ""; - - // Generate dynamic bbox HTML - String bboxHtml = EmailUtils.generateBboxHtml(multipolygon, objectMapper); + // Generate subsetting section (returns empty string if no subsetting) + String subsettingSection = EmailUtils.generateSubsettingSection( + startDate, + endDate, + multipolygon, + objectMapper + ); // Replace all variables in one chain return template .replace("{{uuid}}", uuid) - .replace("{{startDate}}", displayStartDate) - .replace("{{endDate}}", displayEndDate) - .replace("{{bboxContent}}", bboxHtml) + .replace("{{collectionTitle}}", collectionTitle != null ? collectionTitle : "") + .replace("{{fullMetadataLink}}", fullMetadataLink != null ? fullMetadataLink : "") + .replace("{{suggestedCitation}}", suggestedCitation != null ? suggestedCitation : "") + .replace("{{subsettingSection}}", subsettingSection) .replace("{{HEADER_IMG}}", EmailUtils.readBase64Image("header.txt")) .replace("{{DOWNLOAD_ICON}}", EmailUtils.readBase64Image("download.txt")) .replace("{{BBOX_IMG}}", EmailUtils.readBase64Image("bbox.txt")) diff --git a/server/src/main/resources/job-started-email.html b/server/src/main/resources/job-started-email.html index 4281edba..20ab1abc 100644 --- a/server/src/main/resources/job-started-email.html +++ b/server/src/main/resources/job-started-email.html @@ -121,12 +121,12 @@