|
|
@@ -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
|