Skip to content

Commit a77f671

Browse files
committed
Add data export
1 parent 3d92ff8 commit a77f671

2 files changed

Lines changed: 340 additions & 10 deletions

File tree

backend/src/main/java/com/apexgrid/transformertracker/web/InspectionController.java

Lines changed: 276 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
package com.apexgrid.transformertracker.web;
22

3+
import com.apexgrid.transformertracker.ai.ParameterTuningService;
4+
import com.apexgrid.transformertracker.ai.PythonAnalyzerService;
35
import com.apexgrid.transformertracker.model.Inspection;
46
import com.apexgrid.transformertracker.model.Transformer;
57
import com.apexgrid.transformertracker.repo.InspectionRepo;
68
import com.apexgrid.transformertracker.repo.TransformerRepo;
7-
import com.apexgrid.transformertracker.ai.PythonAnalyzerService;
8-
import com.apexgrid.transformertracker.ai.ParameterTuningService;
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.node.ArrayNode;
12+
import com.fasterxml.jackson.databind.node.NullNode;
13+
import com.fasterxml.jackson.databind.node.ObjectNode;
14+
import org.springframework.http.ContentDisposition;
15+
import org.springframework.http.HttpHeaders;
16+
import org.springframework.http.MediaType;
917
import org.springframework.http.ResponseEntity;
1018
import org.springframework.web.bind.annotation.*;
1119
import org.springframework.web.multipart.MultipartFile;
1220

13-
import java.time.Instant;
21+
import javax.imageio.ImageIO;
1422
import java.awt.Graphics2D;
1523
import java.awt.RenderingHints;
1624
import java.awt.image.BufferedImage;
1725
import java.io.ByteArrayInputStream;
18-
import java.util.List;
26+
import java.io.ByteArrayOutputStream;
27+
import java.nio.charset.StandardCharsets;
28+
import java.time.Instant;
1929
import java.util.Base64;
30+
import java.util.List;
2031
import java.util.Map;
21-
import javax.imageio.ImageIO;
22-
import com.fasterxml.jackson.databind.JsonNode;
23-
import com.fasterxml.jackson.databind.ObjectMapper;
24-
import com.fasterxml.jackson.databind.node.ArrayNode;
32+
import java.util.zip.ZipEntry;
33+
import java.util.zip.ZipOutputStream;
2534

2635
@RestController
2736
@RequestMapping("/api/inspections")
@@ -54,6 +63,149 @@ public ResponseEntity<Inspection> getOne(@PathVariable String id) {
5463
.orElse(ResponseEntity.notFound().build());
5564
}
5665

66+
@GetMapping("/{id}/export")
67+
public ResponseEntity<?> exportInspection(@PathVariable String id) {
68+
return repo.findById(id).map(inspection -> {
69+
try {
70+
ObjectMapper mapper = new ObjectMapper();
71+
Instant generatedAt = Instant.now();
72+
73+
ObjectNode root = mapper.createObjectNode();
74+
root.put("exportGeneratedAt", generatedAt.toString());
75+
76+
ObjectNode inspectionNode = root.putObject("inspection");
77+
putNullable(inspectionNode, "id", inspection.getId());
78+
putNullable(inspectionNode, "inspectionNumber", inspection.getInspectionNumber());
79+
putNullable(inspectionNode, "branch", inspection.getBranch());
80+
putNullable(inspectionNode, "status", inspection.getStatus());
81+
putNullable(inspectionNode, "inspectedDate", inspection.getInspectedDate());
82+
putNullable(inspectionNode, "maintainanceDate", inspection.getMaintainanceDate());
83+
putNullable(inspectionNode, "uploadedBy", inspection.getUploadedBy());
84+
putNullable(inspectionNode, "imageUploadedBy", inspection.getImageUploadedBy());
85+
putNullable(inspectionNode, "imageUploadedAt",
86+
inspection.getImageUploadedAt() != null ? inspection.getImageUploadedAt().toString() : null);
87+
putNullable(inspectionNode, "weather", inspection.getWeather());
88+
putNullable(inspectionNode, "lastAnalysisWeather", inspection.getLastAnalysisWeather());
89+
inspectionNode.put("favourite", inspection.isFavourite());
90+
91+
Transformer transformer = inspection.getTransformer();
92+
if (transformer != null) {
93+
ObjectNode transformerNode = inspectionNode.putObject("transformer");
94+
putNullable(transformerNode, "id", transformer.getId());
95+
putNullable(transformerNode, "transformerNumber", transformer.getTransformerNumber());
96+
putNullable(transformerNode, "poleNumber", transformer.getPoleNumber());
97+
putNullable(transformerNode, "region", transformer.getRegion());
98+
putNullable(transformerNode, "type", transformer.getType());
99+
} else {
100+
inspectionNode.putNull("transformer");
101+
}
102+
103+
JsonNode currentBoxes = parseJsonNode(mapper, inspection.getBoundingBoxes());
104+
JsonNode currentFaults = parseJsonNode(mapper, inspection.getFaultTypes());
105+
JsonNode currentAnnotated = parseJsonNode(mapper, inspection.getAnnotatedBy());
106+
JsonNode currentSeverity = parseJsonNode(mapper, inspection.getSeverity());
107+
108+
ObjectNode currentNode = root.putObject("current");
109+
putNullable(currentNode, "timestamp",
110+
inspection.getImageUploadedAt() != null ? inspection.getImageUploadedAt().toString() : generatedAt.toString());
111+
currentNode.set("boundingBoxes", cloneNode(currentBoxes));
112+
currentNode.set("faultTypes", cloneNode(currentFaults));
113+
currentNode.set("annotatedBy", cloneNode(currentAnnotated));
114+
currentNode.set("severity", cloneNode(currentSeverity));
115+
116+
ArrayNode boxHistory = asArrayNode(parseJsonNode(mapper, inspection.getBoundingBoxHistory()));
117+
ArrayNode faultHistory = asArrayNode(parseJsonNode(mapper, inspection.getFaultTypeHistory()));
118+
ArrayNode annotatedHistory = asArrayNode(parseJsonNode(mapper, inspection.getAnnotatedByHistory()));
119+
ArrayNode severityHistory = asArrayNode(parseJsonNode(mapper, inspection.getSeverityHistory()));
120+
ArrayNode timestampHistory = asArrayNode(parseJsonNode(mapper, inspection.getTimestampHistory()));
121+
122+
ArrayNode history = mapper.createArrayNode();
123+
int snapshotCount = maxSize(boxHistory, faultHistory, annotatedHistory, severityHistory, timestampHistory);
124+
for (int index = 0; index < snapshotCount; index++) {
125+
ObjectNode entry = mapper.createObjectNode();
126+
String ts = extractText(timestampHistory, index);
127+
putNullable(entry, "timestamp", ts);
128+
entry.put("isCurrent", false);
129+
entry.set("boundingBoxes", cloneNode(snapshotValue(boxHistory, index)));
130+
entry.set("faultTypes", cloneNode(snapshotValue(faultHistory, index)));
131+
entry.set("annotatedBy", cloneNode(snapshotValue(annotatedHistory, index)));
132+
entry.set("severity", cloneNode(snapshotValue(severityHistory, index)));
133+
history.add(entry);
134+
}
135+
136+
ObjectNode currentEntry = mapper.createObjectNode();
137+
putNullable(currentEntry, "timestamp",
138+
inspection.getImageUploadedAt() != null ? inspection.getImageUploadedAt().toString() : generatedAt.toString());
139+
currentEntry.put("isCurrent", true);
140+
currentEntry.set("boundingBoxes", cloneNode(currentBoxes));
141+
currentEntry.set("faultTypes", cloneNode(currentFaults));
142+
currentEntry.set("annotatedBy", cloneNode(currentAnnotated));
143+
currentEntry.set("severity", cloneNode(currentSeverity));
144+
history.add(currentEntry);
145+
146+
root.set("history", history);
147+
148+
byte[] metadataBytes = mapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(root);
149+
150+
StringBuilder csvBuilder = new StringBuilder();
151+
csvBuilder.append("timestamp,isCurrent,boundingBoxes,faultTypes,annotatedBy,severity\n");
152+
for (JsonNode entry : history) {
153+
String timestamp = entry.path("timestamp").isNull() ? "" : entry.path("timestamp").asText("");
154+
String boxesJson = mapper.writeValueAsString(entry.path("boundingBoxes"));
155+
String faultsJson = mapper.writeValueAsString(entry.path("faultTypes"));
156+
String annotatedJson = mapper.writeValueAsString(entry.path("annotatedBy"));
157+
String severityJson = mapper.writeValueAsString(entry.path("severity"));
158+
csvBuilder.append('"').append(csvEscape(timestamp)).append('"').append(',');
159+
csvBuilder.append(entry.path("isCurrent").asBoolean(false) ? "true" : "false").append(',');
160+
csvBuilder.append('"').append(csvEscape(boxesJson)).append('"').append(',');
161+
csvBuilder.append('"').append(csvEscape(faultsJson)).append('"').append(',');
162+
csvBuilder.append('"').append(csvEscape(annotatedJson)).append('"').append(',');
163+
csvBuilder.append('"').append(csvEscape(severityJson)).append('"').append('\n');
164+
}
165+
byte[] csvBytes = csvBuilder.toString().getBytes(StandardCharsets.UTF_8);
166+
167+
byte[] imageBytes = decodeDataUrl(inspection.getImageUrl());
168+
String imageExt = guessImageExtension(inspection.getImageUrl());
169+
170+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
171+
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
172+
ZipEntry metadataEntry = new ZipEntry("metadata.json");
173+
zos.putNextEntry(metadataEntry);
174+
zos.write(metadataBytes);
175+
zos.closeEntry();
176+
177+
ZipEntry csvEntry = new ZipEntry("history.csv");
178+
zos.putNextEntry(csvEntry);
179+
zos.write(csvBytes);
180+
zos.closeEntry();
181+
182+
if (imageBytes != null && imageBytes.length > 0) {
183+
ZipEntry imageEntry = new ZipEntry("image-original." + imageExt);
184+
zos.putNextEntry(imageEntry);
185+
zos.write(imageBytes);
186+
zos.closeEntry();
187+
}
188+
}
189+
190+
byte[] zipBytes = baos.toByteArray();
191+
String filenameBase = inspection.getInspectionNumber();
192+
if (filenameBase == null || filenameBase.isBlank()) {
193+
filenameBase = inspection.getId();
194+
}
195+
String safeBase = sanitizeFilename(filenameBase);
196+
String downloadName = safeBase + "-export.zip";
197+
198+
HttpHeaders headers = new HttpHeaders();
199+
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
200+
headers.setContentDisposition(ContentDisposition.attachment().filename(downloadName).build());
201+
headers.setContentLength(zipBytes.length);
202+
return ResponseEntity.ok().headers(headers).body(zipBytes);
203+
} catch (Exception ex) {
204+
return ResponseEntity.internalServerError().body(Map.of("error", "Failed to export inspection"));
205+
}
206+
}).orElse(ResponseEntity.notFound().build());
207+
}
208+
57209
@PostMapping
58210
public ResponseEntity<?> create(@RequestBody Inspection i) {
59211
// ensure transformer exists
@@ -406,6 +558,122 @@ private void archivePreviousAnalysis(Inspection i, String annotatedBy) throws Ex
406558
i.setTimestampHistory(timestampHist.toString());
407559
}
408560

561+
private static void putNullable(ObjectNode node, String field, String value) {
562+
if (value == null) {
563+
node.putNull(field);
564+
} else {
565+
node.put(field, value);
566+
}
567+
}
568+
569+
private static JsonNode parseJsonNode(ObjectMapper mapper, String raw) {
570+
if (raw == null || raw.isBlank()) {
571+
return NullNode.getInstance();
572+
}
573+
try {
574+
JsonNode node = mapper.readTree(raw);
575+
return node == null ? NullNode.getInstance() : node;
576+
} catch (Exception ex) {
577+
return NullNode.getInstance();
578+
}
579+
}
580+
581+
private static JsonNode cloneNode(JsonNode node) {
582+
if (node == null || node.isMissingNode()) {
583+
return NullNode.getInstance();
584+
}
585+
return node.deepCopy();
586+
}
587+
588+
private static ArrayNode asArrayNode(JsonNode node) {
589+
return node instanceof ArrayNode ? (ArrayNode) node : null;
590+
}
591+
592+
private static JsonNode snapshotValue(ArrayNode array, int index) {
593+
if (array == null || index < 0 || index >= array.size()) {
594+
return NullNode.getInstance();
595+
}
596+
JsonNode node = array.get(index);
597+
return node == null ? NullNode.getInstance() : node;
598+
}
599+
600+
private static int maxSize(ArrayNode... arrays) {
601+
int max = 0;
602+
if (arrays == null) {
603+
return 0;
604+
}
605+
for (ArrayNode array : arrays) {
606+
if (array != null && array.size() > max) {
607+
max = array.size();
608+
}
609+
}
610+
return max;
611+
}
612+
613+
private static String extractText(ArrayNode array, int index) {
614+
if (array == null || index < 0 || index >= array.size()) {
615+
return null;
616+
}
617+
JsonNode node = array.get(index);
618+
return node != null && node.isValueNode() ? node.asText() : null;
619+
}
620+
621+
private static String csvEscape(String value) {
622+
if (value == null) {
623+
return "";
624+
}
625+
return value.replace("\"", "\"\"")
626+
.replace('\n', ' ')
627+
.replace('\r', ' ');
628+
}
629+
630+
private static byte[] decodeDataUrl(String dataUrl) {
631+
if (dataUrl == null || dataUrl.isBlank()) {
632+
return null;
633+
}
634+
int comma = dataUrl.indexOf(',');
635+
if (comma < 0) {
636+
return null;
637+
}
638+
String base64 = dataUrl.substring(comma + 1);
639+
try {
640+
return Base64.getDecoder().decode(base64);
641+
} catch (IllegalArgumentException ex) {
642+
return null;
643+
}
644+
}
645+
646+
private static String guessImageExtension(String dataUrl) {
647+
if (dataUrl == null || dataUrl.isBlank()) {
648+
return "bin";
649+
}
650+
int colon = dataUrl.indexOf(':');
651+
int semi = dataUrl.indexOf(';');
652+
if (colon >= 0 && semi > colon) {
653+
String mime = dataUrl.substring(colon + 1, semi).toLowerCase();
654+
switch (mime) {
655+
case "image/png":
656+
return "png";
657+
case "image/jpeg":
658+
case "image/jpg":
659+
return "jpg";
660+
case "image/webp":
661+
return "webp";
662+
default:
663+
return "bin";
664+
}
665+
}
666+
return "bin";
667+
}
668+
669+
private static String sanitizeFilename(String value) {
670+
if (value == null || value.isBlank()) {
671+
return "inspection";
672+
}
673+
String sanitized = value.replaceAll("[^A-Za-z0-9._-]", "_");
674+
return sanitized.isBlank() ? "inspection" : sanitized;
675+
}
676+
409677
@PostMapping("/{id}/clear-analysis")
410678
public ResponseEntity<?> clearAnalysis(@PathVariable String id) {
411679
return repo.findById(id).map(i -> {

0 commit comments

Comments
 (0)