View Javadoc
1   package de.dlr.shepard.data.timeseries.utilities;
2   
3   import com.opencsv.CSVWriterBuilder;
4   import com.opencsv.ICSVWriter;
5   import de.dlr.shepard.data.timeseries.io.TimeseriesWithDataPoints;
6   import de.dlr.shepard.data.timeseries.model.TimeseriesTuple;
7   import jakarta.annotation.Nonnull;
8   import java.io.IOException;
9   import java.io.StringWriter;
10  import java.util.List;
11  import java.util.Objects;
12  import java.util.TreeMap;
13  
14  /**
15   * A {@link CsvLineProvider} that provides CSV rows in a column based format, that is each
16   * row of the resulting CSV file represents one timestamp, and each timeseries is put in its
17   * own column.
18   * <p>
19   * It should be noted that this provider may generate very sparse CSV files if the timestamps
20   * of several timeseries do not exactly match
21   */
22  public class CsvColumnLineProvider implements CsvLineProvider {
23  
24    private final StringWriter csvStringWriter;
25    private final ICSVWriter csvWriter;
26  
27    // Number of timeseries
28    private final int size;
29  
30    // True if the header has been written
31    private boolean firstLineWritten = false;
32    private final TimeseriesTuple[] timeseriesArray;
33  
34    // The TreeMap containing all data points is sorted according to the order of the keys (timestamps)
35    private final TreeMap<Long, Object[]> dataPoints = new TreeMap<>();
36  
37    public CsvColumnLineProvider(@Nonnull List<TimeseriesWithDataPoints> timeseriesWithDataPoints) {
38      Objects.requireNonNull(timeseriesWithDataPoints);
39      size = timeseriesWithDataPoints.size();
40      timeseriesArray = new TimeseriesTuple[size];
41  
42      // Sort the data points according to their timestamp
43      for (int i = 0; i < size; i++) {
44        // timeseriesArray is necessary for header generation
45        timeseriesArray[i] = timeseriesWithDataPoints.get(i).getTimeseries();
46        for (var point : timeseriesWithDataPoints.get(i).getPoints()) {
47          // For each individual timestamp, an Object[] is generated and populated
48          dataPoints.computeIfAbsent(point.getTimestamp(), k -> new Object[size])[i] = point.getValue();
49        }
50      }
51  
52      csvStringWriter = new StringWriter();
53      csvWriter = new CSVWriterBuilder(csvStringWriter).build();
54    }
55  
56    @Override
57    public String readCsvLine() throws IOException {
58      // Generate header as first line
59      if (!firstLineWritten) {
60        return generateHeader();
61      }
62  
63      var entry = dataPoints.pollFirstEntry();
64      // If no entry can be polled from map, no more data is available
65      if (entry == null) return "";
66  
67      String[] line = new String[size + 1];
68      // First column is always the timestamp
69      line[0] = entry.getKey().toString();
70  
71      for (int i = 0; i < size; i++) {
72        line[i + 1] = Objects.toString(entry.getValue()[i], "");
73      }
74  
75      csvWriter.writeNext(line, false);
76      var lineBuffer = csvStringWriter.toString();
77      csvStringWriter.getBuffer().setLength(0);
78      return lineBuffer;
79    }
80  
81    /**
82     * Generate the header row
83     * @return One CSV row with the header information.
84     */
85    private String generateHeader() {
86      String[] header = new String[size + 1];
87      header[0] = "timestamp";
88      for (int i = 0; i < size; i++) {
89        header[i + 1] = String.join(
90          "-",
91          timeseriesArray[i].getMeasurement(),
92          timeseriesArray[i].getDevice(),
93          timeseriesArray[i].getLocation(),
94          timeseriesArray[i].getSymbolicName(),
95          timeseriesArray[i].getField()
96        );
97      }
98      csvWriter.writeNext(header, false);
99      firstLineWritten = true;
100     var lineBuffer = csvStringWriter.toString();
101     csvStringWriter.getBuffer().setLength(0);
102     return lineBuffer;
103   }
104 }