|
| 1 | +package au.org.aodn.ogcapi.server.core.util; |
| 2 | + |
| 3 | +import au.org.aodn.ogcapi.features.model.MultipolygonGeoJSON; |
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 5 | +import lombok.extern.slf4j.Slf4j; |
| 6 | +import org.locationtech.jts.geom.Envelope; |
| 7 | +import org.locationtech.jts.geom.MultiPolygon; |
| 8 | +import org.locationtech.jts.geom.Polygon; |
| 9 | + |
| 10 | +import java.io.IOException; |
| 11 | +import java.io.InputStream; |
| 12 | +import java.math.BigDecimal; |
| 13 | +import java.util.List; |
| 14 | +import java.util.Map; |
| 15 | + |
| 16 | +/** |
| 17 | + * Utility for email-related operations |
| 18 | + */ |
| 19 | +@Slf4j |
| 20 | +public class EmailUtils { |
| 21 | + |
| 22 | + /** |
| 23 | + * Read a base64 encoded image from resources |
| 24 | + * @param filename - the filename in /img/ directory |
| 25 | + * @return base64 encoded image as data URL |
| 26 | + * @throws IOException if resource not found |
| 27 | + */ |
| 28 | + public static String readBase64Image(String filename) throws IOException { |
| 29 | + InputStream is = EmailUtils.class.getResourceAsStream("/img/" + filename); |
| 30 | + if (is == null) { |
| 31 | + throw new IOException("Resource not found: /img/" + filename); |
| 32 | + } |
| 33 | + return "data:image/png;base64," + new String(is.readAllBytes()).trim(); |
| 34 | + } |
| 35 | + |
| 36 | + /** |
| 37 | + * Generate HTML content for bounding box section in email |
| 38 | + * @param multipolygon - the multipolygon object |
| 39 | + * @param objectMapper - Jackson ObjectMapper for JSON processing |
| 40 | + * @return HTML string for bbox section |
| 41 | + */ |
| 42 | + public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMapper) { |
| 43 | + try { |
| 44 | + if (multipolygon == null) { |
| 45 | + return buildBboxSection("0", "0", "0", "0", 0); |
| 46 | + } |
| 47 | + |
| 48 | + // Extract coordinates directly from the object |
| 49 | + List<List<List<List<BigDecimal>>>> coordinates = extractCoordinates(multipolygon, objectMapper); |
| 50 | + |
| 51 | + if (coordinates == null || coordinates.isEmpty()) { |
| 52 | + return buildBboxSection("0", "0", "0", "0", 0); |
| 53 | + } |
| 54 | + |
| 55 | + StringBuilder html = new StringBuilder(); |
| 56 | + int bboxCounter = 0; |
| 57 | + |
| 58 | + // Process each polygon separately |
| 59 | + for (List<List<List<BigDecimal>>> polygon : coordinates) { |
| 60 | + // Find min/max for THIS polygon only |
| 61 | + double minLon = Double.MAX_VALUE; |
| 62 | + double maxLon = Double.MIN_VALUE; |
| 63 | + double minLat = Double.MAX_VALUE; |
| 64 | + double maxLat = Double.MIN_VALUE; |
| 65 | + |
| 66 | + for (List<List<BigDecimal>> ring : polygon) { |
| 67 | + for (List<BigDecimal> point : ring) { |
| 68 | + if (point.size() >= 2) { |
| 69 | + double lon = point.get(0).doubleValue(); |
| 70 | + double lat = point.get(1).doubleValue(); |
| 71 | + minLon = Math.min(minLon, lon); |
| 72 | + maxLon = Math.max(maxLon, lon); |
| 73 | + minLat = Math.min(minLat, lat); |
| 74 | + maxLat = Math.max(maxLat, lat); |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + // Use BboxUtils to normalize this polygon's bbox |
| 80 | + MultiPolygon normalizedBbox = BboxUtils.normalizeBbox(minLon, maxLon, minLat, maxLat); |
| 81 | + |
| 82 | + // Build HTML for each normalized bbox |
| 83 | + for (int i = 0; i < normalizedBbox.getNumGeometries(); i++) { |
| 84 | + Polygon normalizedPolygon = (Polygon) normalizedBbox.getGeometryN(i); |
| 85 | + Envelope envelope = normalizedPolygon.getEnvelopeInternal(); |
| 86 | + |
| 87 | + String north = "" + envelope.getMaxY(); |
| 88 | + String south = "" + envelope.getMinY(); |
| 89 | + String west = "" + envelope.getMinX(); |
| 90 | + String east = "" + envelope.getMaxX(); |
| 91 | + |
| 92 | + // Add spacing between multiple bboxes |
| 93 | + if (bboxCounter > 0) { |
| 94 | + html.append("<tr><td style=\"font-size:0;padding:0;word-break:break-word;\">") |
| 95 | + .append("<div style=\"height:24px;line-height:24px;\"> </div>") |
| 96 | + .append("</td></tr>"); |
| 97 | + } |
| 98 | + |
| 99 | + bboxCounter++; |
| 100 | + int displayIndex = (coordinates.size() > 1 || normalizedBbox.getNumGeometries() > 1) ? bboxCounter : 0; |
| 101 | + html.append(buildBboxSection(north, south, west, east, displayIndex)); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + return html.toString(); |
| 106 | + |
| 107 | + } catch (Exception e) { |
| 108 | + log.error("Error generating bbox HTML", e); |
| 109 | + return buildBboxSection("0", "0", "0", "0", 0); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Extract coordinates from multipolygon object (handles both MultipolygonGeoJSON and Map) |
| 115 | + */ |
| 116 | + private static List<List<List<List<BigDecimal>>>> extractCoordinates(Object multipolygon, ObjectMapper objectMapper) throws Exception { |
| 117 | + if (multipolygon instanceof MultipolygonGeoJSON) { |
| 118 | + return ((MultipolygonGeoJSON) multipolygon).getCoordinates(); |
| 119 | + } |
| 120 | + |
| 121 | + if (multipolygon instanceof Map) { |
| 122 | + Map<String, Object> map = (Map<String, Object>) multipolygon; |
| 123 | + Object coords = map.get("coordinates"); |
| 124 | + |
| 125 | + if (coords != null) { |
| 126 | + String coordsJson = objectMapper.writeValueAsString(coords); |
| 127 | + return objectMapper.readValue(coordsJson, |
| 128 | + objectMapper.getTypeFactory().constructParametricType(List.class, |
| 129 | + objectMapper.getTypeFactory().constructParametricType(List.class, |
| 130 | + objectMapper.getTypeFactory().constructParametricType(List.class, |
| 131 | + objectMapper.getTypeFactory().constructParametricType(List.class, BigDecimal.class))))); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return null; |
| 136 | + } |
| 137 | + |
| 138 | + protected static String buildBboxSection(String north, String south, String west, String east, int index) { |
| 139 | + String title = index > 0 ? "Bounding Box " + index : "Bounding Box Selection"; |
| 140 | + |
| 141 | + return "<tr>" + |
| 142 | + "<td align=\"center\" class=\"tr-0\" style=\"background:transparent;font-size:0;padding:0;word-break:break-word;\">" + |
| 143 | + "<table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" border=\"0\" style=\"color:#000000;line-height:normal;table-layout:fixed;width:100%;border:none;\">" + |
| 144 | + "<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"32\">" + |
| 145 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 146 | + "<img alt width=\"32\" style=\"display:block;width:32px;height:32px;\" src=\"{{BBOX_IMG}}\"></td></tr></table></td>" + |
| 147 | + "<td style=\"vertical-align:middle;color:transparent;font-size:0;\" width=\"16\">​</td>" + |
| 148 | + "<td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"auto\">" + |
| 149 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 150 | + "<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 500; line-height: 157%; text-align: left; color: #090c02\">" + |
| 151 | + "<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">" + title + "</p></div></td></tr></table></td></tr>" + |
| 152 | + "</table></td></tr>" + |
| 153 | + "<tr><td style=\"font-size:0;padding:0;word-break:break-word;\"><div style=\"height:8px;line-height:8px;\"> </div></td></tr>" + |
| 154 | + "<tr><td align=\"center\" class=\"tr-0\" style=\"background:transparent;font-size:0;padding:0px 48px 0px 48px;word-break:break-word;\">" + |
| 155 | + "<table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" border=\"0\" style=\"color:#000000;line-height:normal;table-layout:fixed;width:100%;border:none;\">" + |
| 156 | + "<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" + |
| 157 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 158 | + "<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" + |
| 159 | + "<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">N: " + north + "</p></div></td></tr></table></td></tr>" + |
| 160 | + "<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" + |
| 161 | + "<div style=\"height:8px;line-height:8px;\">​</div></td></tr>" + |
| 162 | + "<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" + |
| 163 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 164 | + "<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" + |
| 165 | + "<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">S: " + south + "</p></div></td></tr></table></td></tr>" + |
| 166 | + "<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" + |
| 167 | + "<div style=\"height:8px;line-height:8px;\">​</div></td></tr>" + |
| 168 | + "<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" + |
| 169 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 170 | + "<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" + |
| 171 | + "<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">W: " + west + "</p></div></td></tr></table></td></tr>" + |
| 172 | + "<tr><td style=\"font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;\" aria-hidden=\"true\">" + |
| 173 | + "<div style=\"height:8px;line-height:8px;\">​</div></td></tr>" + |
| 174 | + "<tr><td align=\"left\" class=\"u\" style=\"padding:0;height:auto;word-wrap:break-word;vertical-align:middle;\" width=\"500\">" + |
| 175 | + "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td align=\"left\" width=\"100%\">" + |
| 176 | + "<div style=\"font-family: 'Open Sans', 'Arial', sans-serif; font-size: 14px; font-weight: 400; line-height: 157%; text-align: left; color: #3c3c3c\">" + |
| 177 | + "<p style=\"Margin:0;mso-line-height-alt:22px;font-size:14px;line-height:157%;\">E: " + east + "</p></div></td></tr></table></td></tr>" + |
| 178 | + "</table></td></tr>" + |
| 179 | + "</tr>"; |
| 180 | + } |
| 181 | +} |
0 commit comments