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
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package au.org.aodn.ogcapi.server.core.util;

import au.org.aodn.ogcapi.features.model.MultipolygonGeoJSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

/**
* Utility for email-related operations
*/
@Slf4j
public class EmailUtils {

/**
* Read a base64 encoded image from resources
* @param filename - the filename in /img/ directory
* @return base64 encoded image as data URL
* @throws IOException if resource not found
*/
public static String readBase64Image(String filename) throws IOException {
InputStream is = EmailUtils.class.getResourceAsStream("/img/" + filename);
if (is == null) {
throw new IOException("Resource not found: /img/" + filename);
}
return "data:image/png;base64," + new String(is.readAllBytes()).trim();
}

/**
* Generate HTML content for bounding box section in email
* @param multipolygon - the multipolygon object
* @param objectMapper - Jackson ObjectMapper for JSON processing
* @return HTML string for bbox section
*/
public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMapper) {
try {
if (multipolygon == null) {
return buildBboxSection("0", "0", "0", "0", 0);
}

// Extract coordinates directly from the object
List<List<List<List<BigDecimal>>>> coordinates = extractCoordinates(multipolygon, objectMapper);

if (coordinates == null || coordinates.isEmpty()) {
return buildBboxSection("0", "0", "0", "0", 0);
}

StringBuilder html = new StringBuilder();
int bboxCounter = 0;

// Process each polygon separately
for (List<List<List<BigDecimal>>> polygon : coordinates) {
// Find min/max for THIS polygon only
double minLon = Double.MAX_VALUE;
double maxLon = Double.MIN_VALUE;
double minLat = Double.MAX_VALUE;
double maxLat = Double.MIN_VALUE;

for (List<List<BigDecimal>> ring : polygon) {
for (List<BigDecimal> point : ring) {
if (point.size() >= 2) {
double lon = point.get(0).doubleValue();
double lat = point.get(1).doubleValue();
minLon = Math.min(minLon, lon);
maxLon = Math.max(maxLon, lon);
minLat = Math.min(minLat, lat);
maxLat = Math.max(maxLat, lat);
}
}
}

// Use BboxUtils to normalize this polygon's bbox
MultiPolygon normalizedBbox = BboxUtils.normalizeBbox(minLon, maxLon, minLat, maxLat);

// Build HTML for each normalized bbox
for (int i = 0; i < normalizedBbox.getNumGeometries(); i++) {
Polygon normalizedPolygon = (Polygon) normalizedBbox.getGeometryN(i);
Envelope envelope = normalizedPolygon.getEnvelopeInternal();

String north = "" + envelope.getMaxY();
String south = "" + envelope.getMinY();
String west = "" + envelope.getMinX();
String east = "" + envelope.getMaxX();

// Add spacing between multiple bboxes
if (bboxCounter > 0) {
html.append("<tr><td style=\"font-size:0;padding:0;word-break:break-word;\">")
.append("<div style=\"height:24px;line-height:24px;\">&#8202;</div>")
.append("</td></tr>");
}

bboxCounter++;
int displayIndex = (coordinates.size() > 1 || normalizedBbox.getNumGeometries() > 1) ? bboxCounter : 0;
html.append(buildBboxSection(north, south, west, east, displayIndex));
}
}

return html.toString();

} catch (Exception e) {
log.error("Error generating bbox HTML", e);
return buildBboxSection("0", "0", "0", "0", 0);
}
}

/**
* Extract coordinates from multipolygon object (handles both MultipolygonGeoJSON and Map)
*/
private static List<List<List<List<BigDecimal>>>> extractCoordinates(Object multipolygon, ObjectMapper objectMapper) throws Exception {
if (multipolygon instanceof MultipolygonGeoJSON) {
return ((MultipolygonGeoJSON) multipolygon).getCoordinates();
}

if (multipolygon instanceof Map) {
Map<String, Object> map = (Map<String, Object>) multipolygon;
Object coords = map.get("coordinates");

if (coords != null) {
String coordsJson = objectMapper.writeValueAsString(coords);
return objectMapper.readValue(coordsJson,
objectMapper.getTypeFactory().constructParametricType(List.class,
objectMapper.getTypeFactory().constructParametricType(List.class,
objectMapper.getTypeFactory().constructParametricType(List.class,
objectMapper.getTypeFactory().constructParametricType(List.class, BigDecimal.class)))));
}
}

return null;
}

protected static String buildBboxSection(String north, String south, String west, String east, int index) {
String title = index > 0 ? "Bounding Box " + index : "Bounding Box Selection";

return "<tr>" +
"<td align=\"center\" class=\"tr-0\" style=\"background:transparent;font-size:0;padding:0;word-break:break-word;\">" +
"<table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" border=\"0\" style=\"color:#000000;line-height:normal;table-layout:fixed;width:100%;border:none;\">" +
"<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"32\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<img alt width=\"32\" style=\"display:block;width:32px;height:32px;\" src=\"{{BBOX_IMG}}\"></td></tr></table></td>" +
"<td style=\"vertical-align:middle;color:transparent;font-size:0;\" width=\"16\">&#8203;</td>" +
"<td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"auto\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 500; line-height: 157%; text-align: left; color: #090c02\">" +
"<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">" + title + "</p></div></td></tr></table></td></tr>" +
"</table></td></tr>" +
"<tr><td style=\"font-size:0;padding:0;word-break:break-word;\"><div style=\"height:8px;line-height:8px;\">&#8202;</div></td></tr>" +
"<tr><td align=\"center\" class=\"tr-0\" style=\"background:transparent;font-size:0;padding:0px 48px 0px 48px;word-break:break-word;\">" +
"<table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" border=\"0\" style=\"color:#000000;line-height:normal;table-layout:fixed;width:100%;border:none;\">" +
"<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" +
"<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">N: " + north + "</p></div></td></tr></table></td></tr>" +
"<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" +
"<div style=\"height:8px;line-height:8px;\">&#8203;</div></td></tr>" +
"<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" +
"<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">S: " + south + "</p></div></td></tr></table></td></tr>" +
"<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" +
"<div style=\"height:8px;line-height:8px;\">&#8203;</div></td></tr>" +
"<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" +
"<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">W: " + west + "</p></div></td></tr></table></td></tr>" +
"<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" +
"<div style=\"height:8px;line-height:8px;\">&#8203;</div></td></tr>" +
"<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" +
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" +
"<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" +
"<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">E: " + east + "</p></div></td></tr></table></td></tr>" +
"</table></td></tr>" +
"</tr>";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public ResponseEntity<InlineResponse200> execute(
var recipient = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.RECIPIENT.getValue());

// move the notify user email from data-access-service to here to make the first email faster
restServices.notifyUser(recipient, uuid, startDate, endDate);
restServices.notifyUser(recipient, uuid, startDate, endDate, multiPolygon);

var response = restServices.downloadData(uuid, startDate, endDate, multiPolygon, recipient);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import au.org.aodn.ogcapi.server.core.exception.wfs.WfsErrorHandler;
import au.org.aodn.ogcapi.server.core.model.enumeration.DatasetDownloadEnums;
import au.org.aodn.ogcapi.server.core.service.wfs.DownloadWfsDataService;
import au.org.aodn.ogcapi.server.core.util.EmailUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -15,6 +16,9 @@
import software.amazon.awssdk.services.ses.SesClient;
import software.amazon.awssdk.services.ses.model.*;

import java.io.InputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -36,16 +40,16 @@ public RestServices(BatchClient batchClient, ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public void notifyUser(String recipient, String uuid, String startDate, String endDate) {
public void notifyUser(String recipient, String uuid, String startDate, String endDate, Object multiPolygon) {

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(startDate, endDate)).build();
var content = Content.builder().data(generateStartedEmailContent(uuid, startDate, endDate, multiPolygon)).build();
var destination = Destination.builder().toAddresses(recipient).build();

var body = Body.builder().text(content).build();
var body = Body.builder().html(content).build();
var message = Message.builder()
.subject(subject)
.body(body)
Expand Down Expand Up @@ -106,29 +110,44 @@ private String submitJob(String jobName, String jobQueue, String jobDefinition,
return submitJobResponse.jobId();
}

private String generateStartedEmailContent(String uuid, String startDate, String endDate, Object multipolygon) {
try (InputStream inputStream = getClass().getResourceAsStream("/job-started-email.html")) {

private String generateStartedEmailContent(String startDate, String endDate) {
if (inputStream == null) {
log.error("Email template not found");
throw new RuntimeException("Email template not found");
}

// only include non-empty date conditions
var startDateCondition = "";
var endDateCondition = "";
var dateRangeCondition = "";
if (startDate != null && !startDate.equals("non-specified")) {
startDateCondition = " Start Date: " + startDate + ".";
}
if (endDate != null && !endDate.equals("non-specified")) {
endDateCondition = " End Date: " + endDate + ".";
}
if (!startDateCondition.isBlank() || !endDateCondition.isBlank()) {
dateRangeCondition = "Date range: " + startDateCondition + endDateCondition;
String template = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

// Handle dates - only show if not "non-specified"
String displayStartDate = (startDate != null && !startDate.equals("non-specified")) ? startDate.replace("-", "/") : "";
String displayEndDate = (endDate != null && !endDate.equals("non-specified")) ? endDate.replace("-", "/") : "";

// Generate dynamic bbox HTML
String bboxHtml = EmailUtils.generateBboxHtml(multipolygon, objectMapper);

// Replace all variables in one chain
return template
.replace("{{uuid}}", uuid)
.replace("{{startDate}}", displayStartDate)
.replace("{{endDate}}", displayEndDate)
.replace("{{bboxContent}}", bboxHtml)
.replace("{{HEADER_IMG}}", EmailUtils.readBase64Image("header.txt"))
.replace("{{DOWNLOAD_ICON}}", EmailUtils.readBase64Image("download.txt"))
.replace("{{BBOX_IMG}}", EmailUtils.readBase64Image("bbox.txt"))
.replace("{{TIME_RANGE_IMG}}", EmailUtils.readBase64Image("time-range.txt"))
.replace("{{ATTRIBUTES_IMG}}", EmailUtils.readBase64Image("attributes.txt"))
.replace("{{FACEBOOK_IMG}}", EmailUtils.readBase64Image("facebook.txt"))
.replace("{{INSTAGRAM_IMG}}", EmailUtils.readBase64Image("instagram.txt"))
.replace("{{X_IMG}}", EmailUtils.readBase64Image("x.txt"))
.replace("{{CONTACT_IMG}}", EmailUtils.readBase64Image("email.txt"))
.replace("{{LINKEDIN_IMG}}", EmailUtils.readBase64Image("linkedin.txt"));

} catch (IOException e) {
log.error("Failed to load email template", e);
throw new RuntimeException("Failed to load email template", e);
}

var conditionTitle = dateRangeCondition.isBlank() ? "" : "The conditions of your request include";

return "Your request has been received. " + conditionTitle+ dateRangeCondition +
". Please wait for the result. " +
"After the process is completed, you will receive an email " +
"with the download link.";
}

public SseEmitter downloadWfsDataWithSse(
Expand Down
1 change: 1 addition & 0 deletions server/src/main/resources/img/attributes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAkUExURUdwTGKJp2KMpWWKrWSLpmKLpWKMpWKMpWOMpmKLpWKLpWKLpb74VJIAAAALdFJOUwAg3xA4fKPvxVuQCZPLOwAAAddJREFUSMellb1PwkAYxq8WDehCNBqDiyQwmC5VQ1w7kGjCYsKAiYvExKWTMUwsmLCxEFgcMXEwYZJgbXj+Oe+uvX4cdxXwHUi599fex/s+zxGyZhSGf+StWSaRs4AswhiAhlfU5c178PjWEdc02R/Rn4oGoBOcEUIJTwO46NiEETrguGrzpd69kvWj9i6PlFIjte5MBiy0k3nAlgC623Yy7y1vCbiNC/DTlIFtVhaHP16p8oQc0U+88KcGsLAVlaGTfAmAHbEcLSQBlJeOFWlgIQNToBsBZXd5m1N4FyFwglNjsDTFvu9sxYC6QCsDewIwP3lD555sGegFAzvw6dnmBuJ/PgR2MRQDtOUNeoAhUMAkeLgU9WGiaMVVJOfyqkaBLvr6zuNEJ6M1mbY6dqZ6ASe7vYHiBsCBDgj3Hx0Uk10SKHQnYbF6aiCvqGYKUJY7qUElcFPZoGEa/wDmqwLPOluch4tMmklK3nMhfxXBbCWQv8GsoqnKhwbCzcRXWFBsK8xMxIvj2MQek+sRJXD9yAb7Kts0Iz8sfaivlOyerHPArGtvNFQZ0IqkJwcVbxV4g642XLw8HnTCc4O8Z+uvVZafOVkXLz3gzKv30FL5euob43Vvs19kne9Se2XY1QAAAABJRU5ErkJggg==
1 change: 1 addition & 0 deletions server/src/main/resources/img/bbox.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAMUExURXGSqUdwTEd2lTtuj4sunxQAAAADdFJOU00AqFo58ZcAAABUSURBVDjLYwhFAwwYAvGH/3P/4/+j/8P+w/4H/w9/xSLQjCxw6CtDAAMKYMUUiD+AzGf+ShUBwtZGNSDzmZZSxVoMQ0e9P+r9Qeh9jGxKOGcTLB0AcqanOyPT1xcAAAAASUVORK5CYII=
1 change: 1 addition & 0 deletions server/src/main/resources/img/download.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAACwAAAAsBAMAAADsqkcyAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAtUExURUdwTP///////////////////////////////////////////////////////81e3QIAAAAOdFJOUwBLix7f7oAyxHFCfmCosRnbLAAAALBJREFUKM9jYKAqMBQUXIwpyhz37t0TAwxhtndAkIAhzAQSVhjWwuzb0jJAwm1p2cjBwvIODgKQhDkQwg7IoeoHE0UNWxOYsDNqJPhhU8zAYIpNMdCJcSDRpwXo3ikFCYdj+JILJLwAM+qPvnv3EEs64Xz3bgK29HPuJdZkxV7AQGXAGfcOAzzZwLDvHRbwksEPm/BThndYAUMcdtV92ITfIqILGTQzMCwUxADi1PE7AEkbCZHrk71RAAAAAElFTkSuQmCC
1 change: 1 addition & 0 deletions server/src/main/resources/img/email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAC8AAAAvBAMAAACBCY6fAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAtUExURUdwTP///////////////////////////////////////////////////////81e3QIAAAAOdFJOUwAggPC/jzA4mD/eDtRdxh3/xwAAASZJREFUOMvVkzFuwkAQRQeCbVyA4AZWDoCogtysaCMhlBNYSBa9JS5AldJCouUASeOzcATEghS5+GfIrs1mvYNJnUy5T3//7Mxfov9W45bS5we01CeRj9bKqIfy+a4E5vQEvHHXLjClYYkrBxOIEw0vG0TuuYd4qUEgzi5I5K4CtHdduoipBqG4MsENkOPiYUYGBE1JIrMfoFy2VvBCFgTiywpWDUCpaayvHRrAN5K8EligJFvr4IJKknPgC2hJH2BXpXgXl1FnKde1xABfvTCFFOo0d9rV3XYKoBzd+nVGMlh8HNlINnYizhBDO5D6jRmZRUVsszVge1JZkFkF9jw/ykWDEmcenwTFSQcu4sDTgetB3ke0UBEN2kO9evANXn/7OH+xvgEJevCx3pIXnAAAAABJRU5ErkJggg==
1 change: 1 addition & 0 deletions server/src/main/resources/img/facebook.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAbUExURUdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGnhwewAAAAIdFJOUwDfZp+FuhAgBHVy3QAAAFhJREFUOMtjYECAJEOJDiBgQAdMYGEsEhYd2CVYOnBIVOCS0MAlEYFLQgKXRAd5Em4MmAAsUYBLgoGGEh1IAJdEIy6JVpIlmnFJtOCSaCJOgh5hNSoxgiQARLytLnjnKiwAAAAASUVORK5CYII=
Loading