Skip to content

Commit

Permalink
[BIK-878] Add support for sensor export
Browse files Browse the repository at this point in the history
  • Loading branch information
hb0 committed Jan 10, 2024
1 parent 603b83c commit 8a77f66
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 49 deletions.
147 changes: 105 additions & 42 deletions libs/model/src/main/java/de/cyface/model/Measurement.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 Cyface GmbH
* Copyright 2019-2024 Cyface GmbH
*
* This file is part of the Serialization.
*
Expand Down Expand Up @@ -51,7 +51,7 @@
*
* @author Armin Schnabel
* @author Klemens Muthmann
* @version 2.1.1
* @version 3.0.0
* @since 1.0.0
*/
public class Measurement implements Serializable {
Expand Down Expand Up @@ -190,30 +190,43 @@ public void setTracks(final List<Track> tracks) {
/**
* Exports this measurement as a CSV file.
*
* @param withCsvHeader {@code True} if a CSV header should be added before the data
* @param options The options which describe which data should be exported.
* @param handler A handler that gets one line of CSV output per call
*/
@SuppressWarnings("unused") // API used by backend/executables/cyface-to-csv
public void asCsv(final boolean withCsvHeader, final Consumer<String> handler) {
asCsv(withCsvHeader, null, handler);
public void asCsv(final ExportOptions options, final Consumer<String> handler) {
asCsv(options, null, handler);
}

/**
* Exports this measurement as a CSV file.
*
* @param withCsvHeader {@code True} if a CSV header should be added before the data
* @param options The options which describe which data should be exported.
* @param username The name of the user who uploaded the data or {@code null} to not export this field
* @param handler A handler that gets one line of CSV output per call
*/
public void asCsv(final boolean withCsvHeader, final String username, final Consumer<String> handler) {
if (withCsvHeader) {
if (username == null) {
csvHeaderWithUsername(handler);
} else {
csvHeader(handler);
}
public void asCsv(final ExportOptions options, final String username, final Consumer<String> handler) {
Validate.isTrue(!options.getIncludeUsername() || username != null);

if (options.getIncludeHeader()) {
csvHeader(options, handler);
}

switch (options.getType()) {
case LOCATION:
locationDataAsCsv(options, username, handler);
break;
case ACCELERATION:
case ROTATION:
case DIRECTION:
sensorDataAsCsv(options, username, handler);
break;
default:
throw new IllegalArgumentException(String.format("Unsupported type: %s", options.getType()));
}
}

private void locationDataAsCsv(ExportOptions options, String username, Consumer<String> handler) {
Modality lastModality = Modality.UNKNOWN;

// Iterate through tracks
Expand Down Expand Up @@ -244,15 +257,34 @@ public void asCsv(final boolean withCsvHeader, final String username, final Cons
modalityTypeTravelTime = 0L;
}

handler.accept(csvRow(username, getMetaData(), locationRecord, trackId,
handler.accept(csvRow(options, username, getMetaData(), locationRecord, trackId,
modalityTypeDistance, totalDistance,
modalityTypeTravelTime, totalTravelTime));
handler.accept("\r\n");

lastLocation = locationRecord;
}
}
}

private void sensorDataAsCsv(ExportOptions options, String username, Consumer<String> handler) {
// Iterate through tracks
for (var trackId = 0; trackId < tracks.size(); trackId++) {
final var track = tracks.get(trackId);

// Iterate through sensor points
final var points = options.getType().equals(DataType.ACCELERATION) ? track.getAccelerations()
: options.getType().equals(DataType.ROTATION) ? track.getRotations()
: options.getType().equals(DataType.DIRECTION) ? track.getDirections()
: null;
Validate.notNull(points, "Unsupported type: " + options.getType());
for (final var point : points) {
handler.accept(csvSensorRow(options, username, getMetaData(), point, trackId));
handler.accept("\r\n");
}
}
}

/**
* Exports this measurement as GeoJSON feature.
*
Expand Down Expand Up @@ -428,44 +460,47 @@ private int getIndexOfTrackContaining(final long timestamp) throws TimestampNotF
/**
* Creates a CSV header for this measurement.
*
* @param options The options describing which data is exported
* @param handler The handler that is notified of the new CSV row.
*/
public static void csvHeader(final Consumer<String> handler) {
csvHeader(true, handler);
}

/**
* Creates a CSV header for this measurement.
*
* @param handler The handler that is notified of the new CSV row.
*/
public static void csvHeaderWithUsername(final Consumer<String> handler) {
csvHeader(false, handler);
}

/**
* Creates a CSV header for this measurement.
*
* @param includeUsername {@code True} if the username should be included
* @param handler The handler that is notified of the new CSV row.
*/
private static void csvHeader(final boolean includeUsername, final Consumer<String> handler) {
public static void csvHeader(final ExportOptions options, final Consumer<String> handler) {

final var elements = new ArrayList<String>();
elements.add("userId");
if (includeUsername) {
if (options.getIncludeUserId()) {
elements.add("userId");
}
if (options.getIncludeUsername()) {
elements.add("username");
}
elements.addAll(List.of("deviceId", "measurementId", "trackId", "timestamp [ms]", "latitude", "longitude",
"speed [m/s]", "accuracy [m]", "modalityType", "modalityTypeDistance [m]", "distance [m]",
"modalityTypeTravelTime [ms]", "travelTime [ms]"));
elements.addAll(List.of("deviceId", "measurementId", "trackId", "timestamp [ms]"));
switch (options.getType()) {
case LOCATION:
elements.addAll(List.of("latitude", "longitude",
"speed [m/s]", "accuracy [m]", "modalityType", "modalityTypeDistance [m]", "distance [m]",
"modalityTypeTravelTime [ms]", "travelTime [ms]"));
break;
case ACCELERATION:
elements.addAll(List.of("x [m/s^2]", "y [m/s^2]", "z [m/s^2]"));
break;
case ROTATION:
elements.addAll(List.of("x [rad/s]", "y [rad/s]", "z [rad/s]"));
break;
case DIRECTION:
elements.addAll(List.of("x [uT]", "y [uT]", "z [uT]"));
break;
default:
throw new IllegalArgumentException(String.format("Unsupported type: %s", options.getType()));
}

final var csvHeaderRow = String.join(",", elements);
handler.accept(csvHeaderRow);
handler.accept("\r\n");
}

/**
* Converts one location entry annotated with metadata to a CSV row.
*
* @param options The options which describe which data should be exported.
* @param username the name of the user who uploaded the data or {@code null} to not annotate a username
* @param metaData the {@code Measurement} of the {@param location}
* @param locationRecord the {@code GeoLocationRecord} to be processed
Expand All @@ -476,7 +511,8 @@ private static void csvHeader(final boolean includeUsername, final Consumer<Stri
* @param totalTravelTime the time traveled so far
* @return the csv row as String
*/
private String csvRow(final String username, final MetaData metaData, final RawRecord locationRecord,
private String csvRow(ExportOptions options, final String username, final MetaData metaData,
final RawRecord locationRecord,
final int trackId, final double modalityTypeDistance, final double totalDistance,
final long modalityTypeTravelTime, final long totalTravelTime) {

Expand All @@ -485,8 +521,11 @@ private String csvRow(final String username, final MetaData metaData, final RawR
final var measurementId = String.valueOf(metaData.getIdentifier().getMeasurementIdentifier());

final var elements = new ArrayList<String>();
elements.add(userId.toString());
if (username != null) {
if (options.getIncludeUserId()) {
elements.add(userId.toString());
}
if (options.getIncludeUsername()) {
Validate.notNull(username);
elements.add(username);
}
elements.addAll(List.of(deviceId, measurementId, String.valueOf(trackId),
Expand All @@ -500,6 +539,30 @@ private String csvRow(final String username, final MetaData metaData, final RawR
return String.join(",", elements);
}

private String csvSensorRow(ExportOptions options, final String username, final MetaData metaData,
final Point3DImpl pointRecord,
final int trackId) {

final var userId = metaData.getUserId();
final var deviceId = metaData.getIdentifier().getDeviceIdentifier();
final var measurementId = String.valueOf(metaData.getIdentifier().getMeasurementIdentifier());

final var elements = new ArrayList<String>();
if (options.getIncludeUserId()) {
elements.add(userId.toString());
}
if (options.getIncludeUsername()) {
Validate.notNull(username);
elements.add(username);
}
elements.addAll(List.of(deviceId, measurementId, String.valueOf(trackId),
String.valueOf(pointRecord.getTimestamp()),
String.valueOf(pointRecord.getX()),
String.valueOf(pointRecord.getY()),
String.valueOf(pointRecord.getZ())));
return String.join(",", elements);
}

/**
* Converts a single track to geoJson "coordinates".
*
Expand Down
127 changes: 127 additions & 0 deletions libs/model/src/main/kotlin/de/cyface/model/ExportOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2024 Cyface GmbH
*
* This file is part of the Serialization.
*
* The Serialization is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Serialization is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the Serialization. If not, see <http://www.gnu.org/licenses/>.
*/

package de.cyface.model

/**
* The options which describe which data should be exported.
*
* @author Armin Schnabel
* @version 1.0.0
* @since 3.3.0
* @property format The format in which the data should be exported.
* @property type The type of data which should be exported.
* @property includeHeader `true` if the export should start with a header line, if supported by the [format].
* @property includeUserId `true` if the user id should be included in the export.
* @property includeUsername `true` if the username should be included in the export.
*/
class ExportOptions {
var format: DataFormat = DataFormat.UNDEFINED
var type: DataType = DataType.UNDEFINED
var includeHeader: Boolean = false
var includeUserId: Boolean = false
var includeUsername: Boolean = false

fun format(format: DataFormat): ExportOptions {
this.format = format
return this
}

fun type(type: DataType): ExportOptions {
this.type = type
return this
}

fun includeHeader(includeHeader: Boolean): ExportOptions {
require(!includeHeader || format == DataFormat.CSV) { "Format without header support: $format" }
this.includeHeader = includeHeader
return this
}

fun includeUserId(includeUserId: Boolean): ExportOptions {
this.includeUserId = includeUserId
return this
}

fun includeUsername(includeUsername: Boolean): ExportOptions {
this.includeUsername = includeUsername
return this
}
}

/**
* Supported export data format.
*
* @author Armin Schnabel
* @version 1.0.0
* @since 3.3.0
* @param parameterValue The parameter value which represents the enum option.
*/
enum class DataFormat(@Suppress("MemberVisibilityCanBePrivate") val parameterValue: String) {
/**
* Default value when no format was specified.
*/
UNDEFINED("undefined"),

/**
* Comma separated values.
*/
CSV("csv"),

/**
* JSON format.
*/
@Suppress("unused") // Used
JSON("json")
}

/**
* Supported data types which are used during data collection.
*
* @author Armin Schnabel
* @version 1.0.0
* @since 3.3.0
* @param parameterValue The parameter value which represents the enum option.
*/
enum class DataType(@Suppress("MemberVisibilityCanBePrivate") val parameterValue: String) {
/**
* Default value when no type was specified.
*/
UNDEFINED("undefined"),

/**
* Data collected by the accelerometer.
*/
ACCELERATION("acceleration"),

/**
* Data collected by the gyroscope.
*/
ROTATION("rotation"),

/**
* Data collected by the magnetometer.
*/
DIRECTION("direction"),

/**
* Location data, e.g. collected from a GNSS signal.
*/
LOCATION("location")
}
Loading

0 comments on commit 8a77f66

Please sign in to comment.