Skip to content
Snippets Groups Projects
Commit f32d24bd authored by Lene Wasskog's avatar Lene Wasskog
Browse files

feat: Add observation data columns [VIPSUTV-710]

parent 1d9aad4b
Branches
No related tags found
1 merge request!191Add map module and Open-Meteo support
...@@ -188,25 +188,26 @@ public class ObservationService { ...@@ -188,25 +188,26 @@ public class ObservationService {
@QueryParam("locale") String localeStr, @QueryParam("locale") String localeStr,
@QueryParam("isPositive") Boolean isPositive @QueryParam("isPositive") Boolean isPositive
) { ) {
LOGGER.info("Generate xlsx file for observations");
VipsLogicUser user = getVipsLogicUser(userUUID); VipsLogicUser user = getVipsLogicUser(userUUID);
ULocale locale = new ULocale(localeStr != null ? localeStr : ULocale locale = new ULocale(localeStr != null ? localeStr :
user != null ? user.getOrganizationId().getDefaultLocale() : user != null ? user.getOrganizationId().getDefaultLocale() :
userBean.getOrganization(organizationId).getDefaultLocale()); userBean.getOrganization(organizationId).getDefaultLocale());
LOGGER.info("Generate xlsx file for observations for user {}", user != null ? user.getUserId() : "unregistered");
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
String filenameTimestamp = now.format(formatter); String filenameTimestamp = now.format(formatter);
try { try {
byte[] excelFile = ExcelFileGenerator.generateExcel(getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive), locale); List<ObservationListItem> observations = getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive);
byte[] excelFile = ExcelFileGenerator.generateExcel(observations, locale);
return Response return Response
.ok(excelFile) .ok(excelFile)
.header("Content-Disposition", "attachment; filename=\"" +filenameTimestamp+ "-observations.xlsx\"") .header("Content-Disposition", "attachment; filename=\"" + filenameTimestamp + "-observations.xlsx\"")
.build(); .build();
} catch (IOException e) { } catch (IOException e) {
LOGGER.error(e.getMessage());
return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build(); return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build();
} }
} }
......
package no.nibio.vips.logic.util; package no.nibio.vips.logic.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale;
import no.nibio.vips.logic.entity.rest.ObservationListItem; import no.nibio.vips.logic.entity.rest.ObservationListItem;
import no.nibio.vips.observationdata.ObservationDataSchema;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
...@@ -10,45 +14,186 @@ import java.io.IOException; ...@@ -10,45 +14,186 @@ import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.*;
import java.util.ResourceBundle;
public final class ExcelFileGenerator {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final int COL_INDEX_DATE = 0;
private static final int COL_INDEX_LOCATION = 1;
private static final int COL_INDEX_ORGANISM = 2;
private static final int COL_INDEX_CROP_ORGANISM = 3;
private static final int COL_INDEX_HEADING = 4;
private static final int COL_START_INDEX_DATA = 5;
public static byte[] generateExcel(List<ObservationListItem> observations, ULocale locale) throws IOException {
ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale());
public class ExcelFileGenerator {
public static byte[] generateExcel(List<ObservationListItem> filteredObservationListItems, ULocale locale) throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook(); try (XSSFWorkbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) { ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale()); // Create main mainSheet for all observations, with header row
// Prøvde å bruke i18n, fikk Invalid char (/) found at index (13) in sheet name 'Observasjoner/f?rstefunn' Sheet mainSheet = workbook.createSheet(rb.getString("allObservations"));
Sheet sheet = workbook.createSheet("Observations"); createHeaderRow(mainSheet, rb);
Row headerRow = sheet.createRow(0); int mainSheetRowIndex = 1;
headerRow.createCell(0).setCellValue(rb.getString("timeOfObservation")); // Add one row for each observation in list of all observations
headerRow.createCell(1).setCellValue(rb.getString("organism")); for (ObservationListItem item : observations) {
headerRow.createCell(2).setCellValue(rb.getString("cropOrganismId")); createItemRow(mainSheet, mainSheetRowIndex++, item);
headerRow.createCell(3).setCellValue(rb.getString("observationHeading")); }
autoSizeColumns(mainSheet, 0, COL_INDEX_HEADING);
int rowNum = 1; // Prepare list of observations for each type of pest
for (ObservationListItem item : filteredObservationListItems) { Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations);
// TODO Better way of getting current timezone?
LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
Row row = sheet.createRow(rowNum++); // Create sheets for each individual pest type
row.createCell(0).setCellValue(localDateOfObservation.format(formatter)); for (Integer pestId : pestObservations.keySet()) {
row.createCell(1).setCellValue(item.getOrganismName()); List<ObservationListItem> observationsForPest = pestObservations.get(pestId);
row.createCell(2).setCellValue(item.getCropOrganismName()); ObservationListItem firstObservationForPest = observationsForPest.get(0);
row.createCell(3).setCellValue(item.getObservationHeading()); String pestName = firstObservationForPest.getOrganismName();
Sheet pestSheet = workbook.createSheet(pestName);
Row headerRow = createHeaderRow(pestSheet, rb);
} // Add column titles for observation data
Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema());
int pestSheetColIndex = COL_START_INDEX_DATA;
for (String key : dataColumnTitles.keySet()) {
headerRow.createCell(pestSheetColIndex++).setCellValue(dataColumnTitles.get(key));
}
int pestSheetRowIndex = 1;
for (ObservationListItem item : observationsForPest) {
Row row = createItemRow(pestSheet, pestSheetRowIndex++, item);
// Auto-size columns to fit content if (item.getObservationData() != null) {
for (int i = 0; i <= 3; i++) { Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class);
sheet.autoSizeColumn(i); pestSheetColIndex = COL_START_INDEX_DATA;
for (String key : dataColumnTitles.keySet()) {
Object value = observationDataMap.get(key);
if (value instanceof Number) {
row.createCell(pestSheetColIndex++).setCellValue(((Number) value).intValue());
} else {
row.createCell(pestSheetColIndex++).setCellValue(value != null ? value.toString() : "");
}
}
}
}
autoSizeColumns(pestSheet, COL_INDEX_DATE, COL_START_INDEX_DATA + dataColumnTitles.size());
} }
workbook.write(out); workbook.write(out);
return out.toByteArray(); return out.toByteArray();
} }
} }
/**
* Auto-size columns of given sheet, from startIndex to endIndex, to ensure that content fits
*
* @param sheet The sheet of which to auto-size columns
* @param startIndex The index of the first column to auto-size
* @param endIndex The index of the last column to auto-size
*/
private static void autoSizeColumns(Sheet sheet, int startIndex, int endIndex) {
for (int i = startIndex; i <= endIndex; i++) {
sheet.autoSizeColumn(i);
}
}
/**
* Create map with data property name as key, and corresponding localized name as value
*
* @param observationDataSchema The observation data schema which contains localized names and other info
* @return result map
*/
private static Map<String, String> getObservationDataColumnTitles(ObservationDataSchema observationDataSchema) throws JsonProcessingException {
JsonNode rootNode = objectMapper.readTree(observationDataSchema.getDataSchema());
JsonNode propertiesNode = rootNode.path("properties");
Map<String, String> resultMap = new HashMap<>();
Iterator<Map.Entry<String, JsonNode>> fields = propertiesNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String key = field.getKey();
String value = field.getValue().path("title").asText();
resultMap.put(key, value);
}
return resultMap;
}
/**
* Find the name of the point of interest in given geoInfo string
*
* @param geoInfo The geoInfo which might contain the name of the point of interest
* @return the point of interest name, or empty string
*/
private static String getPointOfInterestName(String geoInfo) throws JsonProcessingException {
JsonNode rootNode = objectMapper.readTree(geoInfo);
JsonNode featuresNode = rootNode.path("features");
if (featuresNode.isArray() && !featuresNode.isEmpty()) {
JsonNode firstFeature = featuresNode.get(0);
JsonNode propertiesNode = firstFeature.path("properties");
return propertiesNode.path("pointOfInterestName").asText();
}
return "";
}
/**
* Create map with pestId as key, and list of corresponding observations as value
*
* @param observations The original list of observations
* @return result map
*/
private static Map<Integer, List<ObservationListItem>> getObservationsForEachPest(List<ObservationListItem> observations) {
Map<Integer, List<ObservationListItem>> pestObservations = new HashMap<>();
for (ObservationListItem observation : observations) {
Integer pestId = observation.getOrganismId();
if (!pestObservations.containsKey(pestId)) {
List<ObservationListItem> observationList = new ArrayList<>();
observationList.add(observation);
pestObservations.put(pestId, observationList);
} else {
pestObservations.get(pestId).add(observation);
}
}
return pestObservations;
}
/**
* Create row with given index, for given observation list item
*
* @param sheet The sheet to which a row will be added
* @param rowIndex The index of the row
* @param item The item of which to add data
* @return the newly created row
*/
private static Row createItemRow(Sheet sheet, int rowIndex, ObservationListItem item) throws JsonProcessingException {
LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
String pointOfInterestName = getPointOfInterestName(item.getGeoInfo());
Row row = sheet.createRow(rowIndex);
row.createCell(COL_INDEX_DATE).setCellValue(localDateOfObservation.format(DATE_FORMATTER));
row.createCell(COL_INDEX_LOCATION).setCellValue(pointOfInterestName);
row.createCell(COL_INDEX_ORGANISM).setCellValue(item.getOrganismName());
row.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(item.getCropOrganismName());
row.createCell(COL_INDEX_HEADING).setCellValue(item.getObservationHeading());
return row;
}
/**
* Create first row of given sheet, with standard set of column titles
*
* @param sheet The sheet to which a row will be added
* @param rb A resource bundle enabling localized messages
* @return the newly created header row
*/
public static Row createHeaderRow(Sheet sheet, ResourceBundle rb) {
Row headerRow = sheet.createRow(0);
headerRow.createCell(COL_INDEX_DATE).setCellValue(rb.getString("timeOfObservation"));
headerRow.createCell(COL_INDEX_LOCATION).setCellValue(rb.getString("location"));
headerRow.createCell(COL_INDEX_ORGANISM).setCellValue(rb.getString("organism"));
headerRow.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(rb.getString("cropOrganismId"));
headerRow.createCell(COL_INDEX_HEADING).setCellValue(rb.getString("observationHeading"));
return headerRow;
}
} }
...@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel ...@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel
observationDataField_unit = Measuring unit observationDataField_unit = Measuring unit
allObservations = All observations
observationDeleted = Observation was deleted observationDeleted = Observation was deleted
observationHeading = Observation heading observationHeading = Observation heading
......
...@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f ...@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f
observationDataField_unit = M\u00e5leenhet observationDataField_unit = M\u00e5leenhet
allObservations = Alle observasjoner
observationDeleted = Observasjonen ble slettet observationDeleted = Observasjonen ble slettet
observationHeading = Observasjonstittel observationHeading = Observasjonstittel
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment