Преглед на файлове

Implementation of PDF certificate

Lukas Cerny преди 10 месеца
родител
ревизия
2b354db9ec

+ 17 - 0
init.sql

@@ -374,3 +374,20 @@ ALTER TABLE tracking.order_to_event OWNER TO senslog;
 
 ALTER TABLE ONLY tracking.order_to_event ADD CONSTRAINT order_event_rec_fk FOREIGN KEY (record_id) REFERENCES tracking.record(id) ON UPDATE CASCADE ON DELETE CASCADE;
 ALTER TABLE ONLY tracking.order_to_event ADD CONSTRAINT order_event_ev_fk FOREIGN KEY (event_id) REFERENCES maplog.event(id) ON UPDATE CASCADE ON DELETE CASCADE;
+
+CREATE TYPE tracking.comparison_operator AS ENUM ('LT', 'LE', 'GE', 'GT', 'NE', 'EQ');
+ALTER TYPE tracking.comparison_operator OWNER TO senslog;
+
+CREATE TABLE tracking.violating_conditions (
+                                               unit_id BIGINT NOT NULL,
+                                               phenomenon_id INTEGER NOT NULL,
+                                               comparison_operator tracking.comparison_operator NOT NULL,
+                                               value DOUBLE PRECISION NOT NULL,
+                                               UNIQUE (unit_id, phenomenon_id)
+);
+
+ALTER TABLE tracking.violating_conditions OWNER TO senslog;
+
+ALTER TABLE ONLY tracking.violating_conditions ADD CONSTRAINT v_cnd_unit FOREIGN KEY (unit_id) REFERENCES maplog.unit(unit_id);
+
+ALTER TABLE ONLY tracking.violating_conditions ADD CONSTRAINT v_cnd_ph FOREIGN KEY (phenomenon_id) REFERENCES maplog.phenomenon(id);

+ 276 - 16
src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java

@@ -2,6 +2,7 @@ package cz.senslog.telemetry.server.ws;
 
 import cz.senslog.telemetry.app.Application;
 import cz.senslog.telemetry.app.PropertyConfig;
+import cz.senslog.telemetry.database.DataNotFoundException;
 import cz.senslog.telemetry.database.PagingRetrieve;
 import cz.senslog.telemetry.database.SortType;
 import cz.senslog.telemetry.database.domain.*;
@@ -28,12 +29,20 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.pdfbox.Loader;
 import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
 import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
 import org.apache.pdfbox.pdmodel.interactive.form.PDField;
 
 
 import java.io.*;
 import java.time.*;
+import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.function.*;
@@ -50,11 +59,15 @@ import static cz.senslog.telemetry.server.ws.AuthorizationType.NONE;
 import static cz.senslog.telemetry.server.ws.ContentType.*;
 import static cz.senslog.telemetry.utils.ListUtils.*;
 import static io.vertx.core.http.HttpHeaders.*;
+import static java.lang.String.format;
 import static java.time.OffsetDateTime.ofInstant;
 import static java.time.format.DateTimeFormatter.*;
+import static java.util.Collections.emptyList;
 import static java.util.Comparator.comparing;
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.*;
+import static org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode.APPEND;
+import static org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA;
 
 public class OpenAPIHandler {
 
@@ -1416,7 +1429,7 @@ public class OpenAPIHandler {
         final int minuteDiff = 30;
 
         final Function<List<UnitTelemetry>, Future<List<UnitTelemetry>>> assignLocation = original -> {
-            if (original.isEmpty()) { return Future.succeededFuture(Collections.emptyList()); }
+            if (original.isEmpty()) { return Future.succeededFuture(emptyList()); }
             List<Future<UnitTelemetry>> futures = new ArrayList<>(original.size());
             Map<Long, List<UnitTelemetry>> staticUnitToTlm = new HashMap<>();
             for (UnitTelemetry t : original) {
@@ -1986,26 +1999,273 @@ public class OpenAPIHandler {
         }
     }
 
-    public void integrationCertificateGET(RoutingContext rc) {
-        String orderIdHash = rc.queryParam("h").get(0);
+    //--------------------- INTEGRATION SECTION ------------------------------
 
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-        try(InputStream is = ResourcesUtils.getInputStream("certificate_template.pdf")) {
-            try (PDDocument doc = Loader.loadPDF(is.readAllBytes())) {
-                PDAcroForm form = doc.getDocumentCatalog().getAcroForm();
-                PDField field = form.getField("text_box");
-                field.setValue(orderIdHash);
+    private enum ComparisonOperator {
+        LT  ("<", "lower than"),
+        LE  ("<=", "lower equal to"),
+        GE  (">=", "greater equal to"),
+        GT  (">", "greater than"),
+        NE  ("!=", "not equal to"),
+        EQ  ("=", "equal to"),
+        ;
+        final String operator;
+        final String label;
+        ComparisonOperator(String operator, String label) {
+            this.operator = operator;
+            this.label = label;
+        }
+        static ComparisonOperator of (String operator) {
+            return valueOf(operator);
+        }
+    }
 
-                form.flatten();
-                doc.save(byteArrayOutputStream);
-            }
-        } catch (IOException e) {
-            throw new HttpIllegalArgumentException(500, e.getMessage());
+    private class CertificateData {
+        final CertificateHeader header;
+        List<ViolationConstrain> violationConstrains;
+        List<ViolationObservation> violationObservations;
+        byte[] trajectoryImage;
+
+        CertificateData(CertificateHeader header) {
+            this.header = header;
+            this.violationConstrains = emptyList();
+            this.violationObservations = emptyList();
         }
+        CertificateData addViolationConstrain(List<ViolationConstrain> violationConstrains) {
+            this.violationConstrains = violationConstrains == null ? emptyList() : violationConstrains;
+            return this;
+        }
+
+        CertificateData addViolationObservations(List<ViolationObservation> violationObservations) {
+            this.violationObservations = violationObservations == null ? emptyList() : violationObservations;
+            return this;
+        }
+
+        CertificateData addTrajectoryImage(byte[] trajectoryImage) {
+            this.trajectoryImage = trajectoryImage;
+            return this;
+        }
+    }
 
-        rc.response().end(Buffer.buffer(byteArrayOutputStream.toByteArray()));
+    private record CertificateHeader(long orderId, long eventId, long unitId, OffsetDateTime trackingStart, OffsetDateTime trackingStop) {}
+
+    private record ViolationConstrain(long sensorId, String sensorName, String sensorUOM, ComparisonOperator compOperator, double value) {}
+
+    private record ViolationObservation(JsonObject observation, OffsetDateTime timestamp, Map<Long, Boolean> violationTable) {}
+
+    public void integrationCertificateGET(RoutingContext rc) {
+        String hParamStr = new String(Base64.getDecoder().decode(rc.queryParam("h").get(0)));
+        if (!hParamStr.matches("-?\\d+(\\.\\d+)?")) {
+            throw new HttpIllegalArgumentException(400, "Invalid input data.");
+        }
+        long orderIdParam = Long.parseLong(hParamStr);
+        MapLogRepository locRepo = (MapLogRepository) repo;
+        locRepo.rawPool()
+                .preparedQuery("SELECT e.id AS event_id, r.order_id, e.unit_id, r.time_tracking_start, r.time_tracking_stop FROM tracking.record r " +
+                "JOIN tracking.order_to_event ote on r.id = ote.record_id " +
+                "JOIN maplog.event e on e.id = ote.event_id " +
+                "WHERE r.order_id = $1 AND r.status = 'DELIVERED' LIMIT 1")
+                .execute(Tuple.of(orderIdParam))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? it.next() : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Order ID '%d' not found.", orderIdParam))))
+                .map(row -> new CertificateData(new CertificateHeader(
+                        row.getLong("order_id"),
+                        row.getLong("event_id"),
+                        row.getLong("unit_id"),
+                        row.getOffsetDateTime("time_tracking_start"),
+                        row.getOffsetDateTime("time_tracking_stop")
+                ))).compose(certificateData -> locRepo.rawPool()
+                        .preparedQuery("SELECT s.sensor_id, s.name AS sensor_name, p.uom, vc.comparison_operator, vc.value FROM maplog.unit u " +
+                        "JOIN maplog.unit_to_sensor uts on uts.unit_id = u.unit_id " +
+                        "JOIN tracking.violating_conditions vc on vc.unit_id = u.unit_id " +
+                        "JOIN maplog.phenomenon p on p.id = vc.phenomenon_id " +
+                        "JOIN maplog.sensor s on s.sensor_id = uts.sensor_id AND s.phenomenon_id = vc.phenomenon_id " +
+                        "WHERE u.unit_id = $1")
+                        .execute(Tuple.of(certificateData.header.unitId()))
+                        .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                                .map(row -> new ViolationConstrain(
+                                        row.getLong("sensor_id"),
+                                        row.getString("sensor_name"),
+                                        row.getString("uom"),
+                                        ComparisonOperator.of(row.getString("comparison_operator")),
+                                        row.getDouble("value")
+                                )).toList()).map(certificateData::addViolationConstrain)
+                ).compose(certificateData -> locRepo.rawPool()
+                        .preparedQuery("SELECT tel.time_stamp, " +
+                                certificateData.violationConstrains.stream().map(c -> String.format(
+                                        " ((tel.observed_values::jsonb ->> %d::bigint::text::varchar)::double precision %s %f)::bool AS \"%d\",", c.sensorId(), c.compOperator().operator, c.value(), c.sensorId()
+                                )).collect(joining()) + " tel.observed_values::json FROM maplog.obs_telemetry AS tel " +
+                                "    JOIN maplog.event AS e on tel.unit_id = e.unit_id " +
+                                "WHERE e.id = $1 AND tel.time_stamp >= e.from_time AND tel.time_stamp < e.to_time "+(certificateData.violationConstrains.isEmpty() ? "" : (" AND (" +
+                                certificateData.violationConstrains.stream().map((c -> String.format(
+                                        " ((tel.observed_values::jsonb ->> %d::bigint::text::varchar)::double precision %s %f) ", c.sensorId(), c.compOperator().operator, c.value()
+                                ))).collect(Collectors.joining("OR")) + ")")) + " ORDER BY tel.time_stamp LIMIT 27")
+                        .execute(Tuple.of(certificateData.header.eventId()))
+                        .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                                .map(row -> new ViolationObservation(
+                                        row.getJsonObject("observed_values"),
+                                        row.getOffsetDateTime("time_stamp"),
+                                        certificateData.violationConstrains.stream()
+                                                .map(c -> Map.entry(c.sensorId(), row.getBoolean(Long.toString(c.sensorId()))))
+                                                .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))
+                                )).toList()).map(certificateData::addViolationObservations)
+                ).compose(certificateData -> Future.succeededFuture(ResourcesUtils.getBytes("no_trajectory.png"))
+                        .map(certificateData::addTrajectoryImage)
+                ).onSuccess(certificateData -> {
+                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                    try(InputStream is = ResourcesUtils.getInputStream("theros_template.pdf")) {
+                        try (PDDocument doc = Loader.loadPDF(is.readAllBytes())) {
+                            long orderId = certificateData.header.orderId();
+                            long unitId = certificateData.header.unitId();
+                            OffsetDateTime trackingStart = certificateData.header.trackingStart();
+                            OffsetDateTime trackingStop = certificateData.header.trackingStop();
+                            byte[] trajectoryImage = certificateData.trajectoryImage;
+                            boolean isCertified, areConstrainsApplied;
+                            String transportRes, transportConditions;
+
+                            if (certificateData.violationConstrains.isEmpty()) {
+                                areConstrainsApplied = false;
+                                isCertified = true;
+                            } else {
+                                areConstrainsApplied = true;
+                                isCertified = certificateData.violationObservations.isEmpty();
+                            }
+
+                            if (areConstrainsApplied) {
+                                String prefix = isCertified ? "no" : "";
+                                transportConditions = certificateData.violationConstrains.stream()
+                                        .map(c -> String.format("%s %s %s %.1f%s", prefix, c.sensorName().toLowerCase(), c.compOperator().label, c.value(), c.sensorUOM()))
+                                        .collect(joining(" | "));
+                            } else {
+                                transportConditions = "No transporting conditions were applied.";
+                            }
+
+                            if (isCertified) {
+                                transportRes = "The transporting conditions were maintained during the delivery.";
+                            } else {
+                                transportRes = "The transporting conditions were violated during the delivery.";
+                            }
+
+                            PDAcroForm form = doc.getDocumentCatalog().getAcroForm();
+                            form.getField("order_id").setValue(Long.toString(orderId));
+                            form.getField("unit_id").setValue(Long.toString(unitId));
+                            form.getField("tracking_start").setValue(trackingStart.toLocalDateTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
+                            form.getField("tracking_stop").setValue(trackingStop.toLocalDateTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
+                            form.getField("transporting_result").setValue(transportRes);
+                            form.getField("transporting_conditions").setValue(transportConditions);
+
+                            try (PDPageContentStream contents = new PDPageContentStream(doc, doc.getPage(0), APPEND, true)) {
+                                PDImageXObject trajectoryObj = PDImageXObject.createFromByteArray(doc, trajectoryImage, "trajectory");
+                                contents.drawImage(trajectoryObj, 80, 180, 450, 300);
+
+                                byte[] stampImgBytes = ResourcesUtils.getBytes(isCertified ? "certified.png" : "discredited.png");
+                                PDImageXObject stampObj = PDImageXObject.createFromByteArray(doc, stampImgBytes, "stamp");
+                                contents.drawImage(stampObj, 380, 110, 177, 150);
+
+                            }
+
+                            if (!isCertified) {
+                                // set alert table
+                                Map<Long, String> constrainAlert = certificateData.violationConstrains.stream()
+                                        .map(c -> Map.entry(c.sensorId(), String.format("%s %s %.1f%s", c.sensorName(), c.compOperator().label, c.value(), c.sensorUOM())))
+                                        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+                                List<String[]> tableContent = certificateData.violationObservations.stream()
+                                        .flatMap(o -> o.violationTable().entrySet().stream()
+                                                .filter(Map.Entry::getValue)
+                                                .map(Map.Entry::getKey)
+                                                .map(sensorId -> new String[]{
+                                                        o.timestamp().format(DateTimeFormatter.ofPattern("HH:mm dd.MM.yyyy")),
+                                                        String.format("%s (was %.1f)", constrainAlert.get(sensorId), o.observation().getDouble(Long.toString(sensorId)))
+                                                })).toList();
+
+                                if (tableContent.size() > 26) {
+                                    tableContent = new ArrayList<>(tableContent.subList(0, 26));
+                                    tableContent.add(new String[]{"...", "..."});
+                                }
+
+                                PDPage page = doc.getPage(1);
+                                try (PDPageContentStream contentStream = new PDPageContentStream(doc, page, APPEND, true)) {
+                                    // Set up some basic parameters
+                                    float margin = 80;
+                                    float yStart = 710; // starting y position
+                                    float tableWidth = page.getMediaBox().getWidth() - (2 * margin);
+                                    float rowHeight = 25;
+                                    float col1Width = 130;
+                                    float col2Width = tableWidth - col1Width;
+
+                                    float[] colWidths = {col1Width, col2Width};
+
+                                    // Set a font and font size
+                                    contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 14);
+
+                                    // Draw the table row by row
+                                    for (int row = 0; row < tableContent.size(); row++) {
+                                        // Y coordinate (top line of the current row)
+                                        float currentRowY = yStart - (rowHeight * row);
+
+                                        // Draw the horizontal line (top/bottom of each row)
+                                        contentStream.moveTo(margin, currentRowY);
+                                        contentStream.lineTo(margin + tableWidth, currentRowY);
+                                        contentStream.stroke();
+
+                                        // If it's the last row in the loop, also draw the bottom line
+                                        if (row == tableContent.size() - 1) {
+                                            float bottom = currentRowY - rowHeight;
+                                            contentStream.moveTo(margin, bottom);
+                                            contentStream.lineTo(margin + tableWidth, bottom);
+                                            contentStream.stroke();
+                                        }
+
+                                        // Draw each cell in this row
+                                        float xPos = margin; // left boundary for the first column
+                                        for (int col = 0; col < colWidths.length; col++) {
+                                            float colWidth = colWidths[col];
+
+                                            // Draw the vertical line for the left edge of the cell
+                                            contentStream.moveTo(xPos, currentRowY);
+                                            contentStream.lineTo(xPos, currentRowY - rowHeight);
+                                            contentStream.stroke();
+
+                                            // If it's the last column, draw the right boundary
+                                            if (col == colWidths.length - 1) {
+                                                float cellRightX = xPos + colWidth;
+                                                contentStream.moveTo(cellRightX, currentRowY);
+                                                contentStream.lineTo(cellRightX, currentRowY - rowHeight);
+                                                contentStream.stroke();
+                                            }
+
+                                            // Write text in the cell
+                                            float textX = xPos + 5;       // small horizontal margin
+                                            float textY = currentRowY - 18; // approx 14 points down for baseline
+
+                                            contentStream.beginText();
+                                            contentStream.newLineAtOffset(textX, textY);
+                                            contentStream.showText(tableContent.get(row)[col]);
+                                            contentStream.endText();
+
+                                            // Move xPos to the next column start
+                                            xPos += colWidth;
+                                        }
+                                    }
+                                }
+                            } else {
+                                doc.removePage(1);
+                            }
+
+                            form.flatten();
+                            doc.save(byteArrayOutputStream);
+                        }
+                    } catch (IOException e) {
+                        throw new HttpIllegalArgumentException(500, e.getMessage());
+                    }
+
+                    rc.response().end(Buffer.buffer(byteArrayOutputStream.toByteArray()));
+                })
+                .onFailure(rc::fail);
     }
-//--------------------- INTEGRATION SECTION ------------------------------
 
     private enum ManualTrackingAction {
         START, STOP

+ 15 - 0
src/main/java/cz/senslog/telemetry/utils/ResourcesUtils.java

@@ -1,5 +1,6 @@
 package cz.senslog.telemetry.utils;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -14,4 +15,18 @@ public final class ResourcesUtils {
         ClassLoader classloader = Thread.currentThread().getContextClassLoader();
         return classloader.getResourceAsStream(resourceFile);
     }
+
+    public static byte[] getBytes(String resourceFile) {
+        InputStream stream = getInputStream(resourceFile);
+        if (stream == null) {
+            return new byte[0];
+        }
+        try {
+            byte[] bytes = stream.readAllBytes();
+            stream.close();
+            return bytes;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }

BIN
src/main/resources/certified.png


BIN
src/main/resources/discredited.png


BIN
src/main/resources/no_trajectory.png


BIN
src/main/resources/theros_template.pdf