diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0a93004 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Standard CI + +on: + push: + branches: + - main + - release + pull_request: + branches: + - main + - release + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Download & unzip protoc + run: | + wget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.0/protoc-3.17.0-linux-x86_64.zip \ + -O protoc-3.zip + unzip protoc-3.zip -d protoc-3 + mv protoc-3/bin/protoc protoc + + - name: Compile java files from `proto` file + run: | + mkdir -p src/main/java + ./protoc --java_out=./src/main/java/ src/main/protos/de/cyface/protos/model/measurement.proto + + - name: Build with Gradle + run: ./gradlew build + env: + USERNAME: ${{ github.actor }} + PASSWORD: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-jar.yml b/.github/workflows/publish-jar.yml new file mode 100644 index 0000000..80fbf66 --- /dev/null +++ b/.github/workflows/publish-jar.yml @@ -0,0 +1,45 @@ +name: Publish to GitHub Packages + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Download & unzip protoc + run: | + wget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.0/protoc-3.17.0-linux-x86_64.zip \ + -O protoc-3.zip + unzip protoc-3.zip -d protoc-3 + mv protoc-3/bin/protoc protoc + + - name: Compile java files from `proto` file + run: | + ./protoc --java_out=./src/main/java/ src/main/protos/de/cyface/protos/model/measurement.proto + + # Publish slim JARS to Github Package Registry + - name: Publish package + run: ./gradlew publish + env: + USERNAME: ${{ github.actor }} + PASSWORD: ${{ secrets.GITHUB_TOKEN }} + + # Automatically mark this tag as release on Github + - uses: actions/create-release@v1 + id: create_release + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + # Release tags of format `1.2.3-beta1 / -alpha1 / -test1` are considered a pre-release + prerelease: ${{ contains(github.ref, 'test') || contains(github.ref, 'alpha') || contains(github.ref, 'beta') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8de083 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# +# Copyright 2018 Cyface GmbH +# +# This file is part of the Cyface Data Collector. +# +# The Cyface Data Collector 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 Cyface Data Collector 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 Cyface Data Collector. If not, see . +# +# Eclipse Files +.classpath +.settings +bin +.project + +# Intellij Idea Files +.idea +.editorconfig +out +*.iml + +# Gradle Temporary Files +.gradle +build +local.properties +gradle.properties + +# Ignore local test maven repositories +**/repo + +# Vim Temporary Files +**/*.swp + +# Cyface Temporary Files +**/file-uploads +**/.vertx +**/conf.json +**/*.log +**/secrets +*.json + +# Generated java classes from proto files +src/main/java/ + +# Temporary Git Files +**/*.orig + +# Ignore logging configurations +**/logback.xml + +# Vertx Config files +**/config.json diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..096e9a7 --- /dev/null +++ b/README.adoc @@ -0,0 +1,75 @@ += Cyface Protocol Buffer Messages + +This repository defines the Cyface Models as schemas such as Protocol Buffer Schemas. +This allows to de-/serialize the Cyface Binary Data from in different programming languages. + +For more details on Protocol Buffers (short: Protobuf) check out it's link:https://developers.google.com/protocol-buffers[documentation]. + + +== Message Format Definitions + +This is a collection of `.proto` files describe the data structure of the Cyface Models. + +- This allows Protobuf to automatically de-/encode the the data from/to binary files. +- Supports later extensions of the format with backward-compatibility +- Following the link:https://developers.google.com/protocol-buffers/docs/style[Style Guide] + +At the time of writing the latest release is link:https://developers.google.com/protocol-buffers/docs/proto3[Protocol Buffer Version 3]. + + +=== Notes +Reminders from the documentation which need to be considered for future updates: + + - optional fields are default in proto3, `required` fields have been removed. + + - link:https://developers.google.com/protocol-buffers/docs/proto3#updating[Updating A Message Type] + + - link:https://developers.google.com/protocol-buffers/docs/javatutorial#extending-a-protocol-buffer[Extending a Protocol Buffer] + + - Deprecated field link:https://developers.google.com/protocol-buffers/docs/proto3#options[annotations] + + - link:https://developers.google.com/protocol-buffers/docs/proto3#default[Default values] + + - Scalar message fields, once parsed, cannot tell if a field was not set or set the the default value afterwards. + - Default values are not serialized on the wire. + - Keep this in mind when defining e.g. booleans, make sure you want that "default" `false` behavior on. + +Other message types: + + - link:https://developers.google.com/protocol-buffers/docs/proto3#using_oneof[One-of] if only one of many fields can be set / should be interpreted. link:https://developers.google.com/protocol-buffers/docs/proto3#backwards-compatibility_issues[Be careful] with these fields. + - link:https://developers.google.com/protocol-buffers/docs/proto3#maps[Maps] + - link:https://developers.google.com/protocol-buffers/docs/proto3#json[JSON Mapping] + - There are also "well known" message types from Google, e.g. for `JSON` + - link:https://developers.google.com/protocol-buffers/docs/javatutorial#advanced-usage[Reflections] + + +== Compiling the Message Definitions + +Generates serializer, deserializer, etc. in a chosen language, e.g. `.java` files for Java. + +Java classes can be compiled with link:https://developers.google.com/protocol-buffers/docs/javatutorial#compiling-your-protocol-buffers[protoc] (Protocol Buffers `v3.17.0`): + + protoc --java_out=./src/main/java/ src/main/protos/de/cyface/protos/model/measurement.proto + +However, the pre-compiled `JARs` are also published to the link:https://github.com/orgs/cyface-de/packages?repo_name=protos[Github Package Registry]. + +- how to use with link:https://github.com/protocolbuffers/protobuf/tree/master/java#gradle[Gradle] +- how to use with link:https://github.com/protocolbuffers/protobuf/tree/master/java#use-java-protocol-buffers-on-android[Android] + +The serializers encode the data in an efficient way, the decision process is documented link:https://cyface.atlassian.net/wiki/spaces/IM/pages/1535148033/Datenformat+bertragungsprotokoll+2021[internally]. + + +== Using the generated Code + +____ +Protocol Buffers and Object Oriented Design Protocol buffer classes are basically dumb data holders (like structs in C); they don't make good first class citizens in an object model. If you want to add richer behavior to a generated class, the best way to do this is to wrap the generated protocol buffer class in an application-specific class. +____ +link:https://developers.google.com/protocol-buffers/docs/javatutorial#builders[Source] + + +[#_licensing] +== Licensing + +Copyright (C) 2021 Cyface GmbH - All Rights Reserved +Unauthorized copying of this file, via any medium is strictly prohibited +Proprietary and confidential diff --git a/README.md b/README.md deleted file mode 100644 index 5be06fa..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# protos -All Prototol Buffer files (.proto) for our serializations. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9acac47 --- /dev/null +++ b/build.gradle @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Cyface GmbH + * + * This file is part of the Cyface Protocol Buffer Messages. + * + * The Cyface Protocol Buffer Messages 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 Cyface Protocol Buffer Messages 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 Cyface Protocol Buffer Messages. If not, see . + */ +/** + * The root build gradle file. + * + * @author Armin Schnabel + */ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'java' + id 'application' + id 'eclipse' + id 'idea' + id 'maven-publish' + //noinspection SpellCheckingInspection + id 'com.github.johnrengelman.shadow' version '6.1.0' apply false +} + +group = 'de.cyface' +version = '0.0.0' + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +ext { + commonsLangVersion = '3.8.1' + gradleWrapperVersion = '6.8.3' + protobufVersion = '3.17.0' + + // Versions of testing dependencies + junitVersion = '5.7.0' + mockitoVersion = '3.3.3' + hamcrestVersion = '2.2' + flapdoodleVersion = '3.0.0' +} + +wrapper { + gradleVersion = "$gradleWrapperVersion" +} + +repositories { + mavenCentral() +} + +// Code Quality Checker +dependencies { + // Protobuf generated java files + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + + // Utility + implementation "org.apache.commons:commons-lang3:$commonsLangVersion" // Using Validate + + // Testing Dependencies + testImplementation(platform("org.junit:junit-bom:$junitVersion")) + testImplementation "org.junit.jupiter:junit-jupiter-api" + //testImplementation "org.junit.jupiter:junit-jupiter-params" // Required for parameterized tests + testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +// Definitions for the maven-publish Plugin +publishing { + // The following repositories are used to publish artifacts to. + repositories { + maven { + name = 'github' + url = uri("https://maven.pkg.github.com/cyface-de/protos") + credentials { + username = project.findProperty("gpr.user") ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") ?: System.getenv("PASSWORD") + } + } + maven { + name = 'local' + url = "file://${rootProject.buildDir}/repo" + } + } +} \ No newline at end of file diff --git a/gradle.properties.template b/gradle.properties.template new file mode 100644 index 0000000..45a31b3 --- /dev/null +++ b/gradle.properties.template @@ -0,0 +1,2 @@ +gpr.user= +gpr.key= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..442d913 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0426832 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Cyface GmbH + * + * This file is part of the Cyface Protocol Buffer Messages. + * + * The Cyface Protocol Buffer Messages 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 Cyface Protocol Buffer Messages 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 Cyface Protocol Buffer Messages. If not, see . + */ + +//noinspection SpellCheckingInspection +rootProject.name = 'protos' diff --git a/src/main/protos/de/cyface/protos/model/measurement.proto b/src/main/protos/de/cyface/protos/model/measurement.proto new file mode 100644 index 0000000..397e36a --- /dev/null +++ b/src/main/protos/de/cyface/protos/model/measurement.proto @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2021 Cyface GmbH - All Rights Reserved + * + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +syntax = "proto3"; + +/* + * More details on packages: https://developers.google.com/protocol-buffers/docs/proto3#packages + * And Style Guide Packages section: https://developers.google.com/protocol-buffers/docs/style#packages + */ +package de.cyface.protos.model; + +/* + * To generate own `.java` files for the top level classes/enums. + * + * More details on options: https://developers.google.com/protocol-buffers/docs/proto3#options + * + * Import messages: + * - In java you cannot import message types from other `.proto` files. + * see https://developers.google.com/protocol-buffers/docs/proto3#importing_definitions + * - "Any" for arbitrary serialized messages as bytes without having their `proto` definitions + * see https://developers.google.com/protocol-buffers/docs/proto3#any + */ +option java_multiple_files = true; + +/* + * A message type which wraps all data collected between a "start" and a "stop" lifecycle event. + * + * We are reserving field numbers 1-15 for high-frequent fields (> 10 Hz) that we might want to add later. + * - The `reserved` definition is not used, as "field numbers cannot be used by future users" + * see https://developers.google.com/protocol-buffers/docs/proto3#updating + * - The following fields are not repeating: formatVersion, locationsRecords, accelerations/r./d., capturingLog + * - The following fields are expected less than 10 Hz: events, images, videos + * + * author Armin Schnabel + * Version 1.0.0 + */ +message Measurement { + /* + * Using `uint32` as we expect the version to be positive and within the range of an `integer`. + * Negative numbers are encoded inefficiently in this data type, compared to `sint32`. + * + * The current format of the binary format version is `2` + */ + uint32 format_version = 16; + + /* + * Collection of geo-locations. + * + * We use a packed encoding here, so this is a non repeated field. + */ + LocationRecords location_records = 17; + /* + * Collection of acceleration points. + * + * We use a packed encoding here, so this is a non repeated field. + */ + Accelerations accelerations = 18; + /* + * Collection of rotation points. + * + * We use a packed encoding here, so this is a non repeated field. + */ + Rotations rotations = 19; + /* + * Collection of direction points. + * + * We use a packed encoding here, so this is a non repeated field. + */ + Directions directions = 20; + + /* + * Collection of events. + * + * We use `repeated` as we don't expect as many events as e.g. sensor points. + * If we decide to store high-frequent events later on we can use a field number < 16 which only requires 1 Byte. + */ + repeated Event events = 21; + + /* + * We expect a similar amount of images as e.g. locations (<= 1 Hz). + * But the image data is anyway very large, thus, we don't have to care too much about the field number encoding size and use `repeated` complex types. + * + * The image files are usually of FileType `JPG` (compressed) or `DNG` (raw). + */ + repeated File images = 22; + /* + * When a measurement is paused and resumed, multiple video files are collected per measurement - thus, `repeated`. + * + * The video files are usually of FileType `MP4`. + */ + repeated File videos = 23; + /* + * While capturing images we log the capturing frequencies and some more data. + * + * This field allows to attach this file which is usually a of FileType `CSV`. + */ + File capturing_log = 24; +} + +/* + * A message type which wraps all geo-locations captured for one measurement. + * + * Each field contains the ordered list of that attribute for all points. + * I.e. field1[4] and field2[4] belong to the same point. + * + * Reason why we don't use `repeated LocationRecord` (and the same for sensor data): + * - In proto3 repeated fields use the "packed" encoding by default. + * A field with 0 elements is not appearing in the message. + * A field with >0 elements and a "primitive numeric type" use a single key-value pair with "wire type 2(length-delimited)". + * Fields with non-primitive types (like `LocationRecord` would be) encode the `key` for each repetition of `LocationRecord`. + * Example: LR KV KV, LR KV ... with LR = field number of LocationRecord and KV is a key-value pair inside this record. + * i.e.: LocationRecord{ts=1234567890123,lat=51012345,lon=13012345}, LocationRecord{ts=1000,lat=34,lon=54} + * Bytes: 56-66 KiB (without/with elevations) for a measurement with 3600 Locations (and no sensor data) + * - We use a more efficient way: One LR-entry with multiple primitive `packed` fields. + * Example: LR K V V V V V K V V V V V, i.e. we only have to encode the type "LocationRecord" once not for each record. + * i.e: LocationRecords { ts=[1234567890123,1000,1000], lat=[51012345,34,35], lon=[13012345,54,55]} + * Bytes: 32-49 KiB (without/with elevations) for a measurement with 3600 Locations (and no sensor data) + * - In this annotation each LR must have a ts/lat/lon/speed/accuracy or else this does not work. + * Only for `elevation` we define a `Elevation` message type which can also have no value and would encoded: `E E KV` or `E KV E KV` + */ +message LocationRecords { + /* + * Using `uint64` as we expect the timestamps to be in order, i.e. usually only ascending. + * Negative numbers are encoded inefficiently in this data type, compared to `sint32`. + * `64bit` as the first timestamp is absolute and in milliseconds since 1970, i.e. it's not within the boundaries of `integer`. + * + * The timestamps are encoded in the offset/diff format, e.g.: 1234567890123,1000,1000,1000. + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated uint64 timestamp = 1; + + /* + * Using `sint32` as the latitude can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * `32bit` as the maximum expected value is the diff between -180 and +180 which is: 360_000_000 and within `integer` range. + * + * The coordinate-part is encoded in the offset/diff format, e.g.: + * (51.012345,13.012300),(51.012300,13.012345) => lat=[51012345,-45], lon=[13012300,45] + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated sint32 latitude = 2; + + /* + * Using `sint32` as the latitude can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * `32bit` as the maximum expected value is the diff between -90 and +90 which is: 180_000_000 and within `integer` range. + * + * The coordinate-part is encoded in the offset/diff format, e.g.: + * (51.012345,13.012300),(51.012300,13.012345) => lat=[51012345,-45], lon=[13012300,45] + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated sint32 longitude = 3; + + /* + * Using a complex type `Elevation` for elevations as we expect some elevations to be `null`. + * - Due to the "collection" format of the `LocationRecords` we cannot use a `repeated sint32 elevation` field as this does not support `null` entries. + * + * We use 1-byte field numbers (1-15) for `elevation` and the `Elevation` fields as this is a repeated complex field, i.e. these numbers are encoded for each location. + */ + repeated Elevation elevation = 4; + + /* + * The encoding should look like the following: + * - E 1 48000 E 1 100 E 1 -50 (scenario where all locations have elevations) + * - E 1 48000 E 2 1 E 1 100 (scenario where some locations have elevations) + * - 0 bytes (scenario without elevations) + */ + message Elevation { + /* + * Using `sint32` as the elevation can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * `32bit`, i.e. we support a diff value up to ~2*10^9 cm which is ~ 20.000 km. The diff between mount everest + * and the dead sea is ~ [8848m;-414m] so we should be good when teleporting between dry and comfy places on earth for a while. + * + * The elevation is encoded in the offset/diff format, e.g.: + * 480.0m, 481.0m, 480.5m => 48000 cm, 100cm, -50cm + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * Elevation in cm above sea level, which can also be negative (as absolute number, beside the diff-annotation used here) + */ + sint32 value = 1; + + /* + * This field is only encoded if no `value` is set (i.e. elevation `is_null`). + * + * We cannot just have the optional "value" field as we can't differentiate between default value (0) and unset (0) + */ + bool is_null = 2; + } + + /* + * Using `sint32` as the accuracy can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * `32bit`, i.e. we support a diff value up to ~2*10^9 cm which is ~ 20.000 km. + * + * The accuracy is encoded in the offset/diff format, e.g.: + * 8.0m, 13.0m, 12.5m => 800 cm, 500cm, -50cm + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * Accuracy in cm. Can not be smaller than 0 (as absolute number, beside the diff-annotation used here). + * + * The likelihood of that accuracy to be correct is not stored as not all platforms provide this or provide this in different format. + * For some platform this is static, i.e. we can retrieve this information from the osVersion/osType field. + */ + repeated sint32 accuracy = 5; + + /* + * Using `sint32` as the speed can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * `32bit`, i.e. we support a diff value up to ~2*10^9 cm/s which is ~ 20.000 km/s. + * As we capture the speed relative to the earth's rotation, we should be covering all manned aircraft, but not spacecrafts. + * + * The speed is encoded in the offset/diff format, e.g.: + * 10.0m/s, 11.0m, 10.5m => 1000 cm/s, 100cm, -50cm + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * Speed in cm/s. Should not be smaller than 0 (as absolute number, beside the diff-annotation used here), + * but we saw on some platforms (Samsung/Android?) that negative speed is recorded, but this does not break this format. + */ + repeated sint32 speed = 6; +} + +/* + * A message type which wraps all acceleration points captured for one measurement. + * + * Each field contains the ordered list of that attribute for all points. + * I.e. field1[4] and field2[4] belong to the same point. + * + * Reference phone, Pixel 3a, contains this sensor: https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi160/ + * - Bosch recommends the following sensor: https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/ + * - the sensitivity and value ranges are the same [DAT-646] + * - the sensitivity is 0.004790957 i.e. ~ 1/208.73 ... which is 1÷(16384 [from datasheet] ÷9,81 [g to m/s])×8 [8 bits per byte/digit] + * - Thus, we choose a unit value of 0.001 m/s^2, i.e 1mm/s^2. + */ +message Accelerations { + /* + * Using `uint` as we expect the timestamps to be in order, i.e. usually only ascending. + * Negative numbers are encoded inefficiently in this data type, compared to `sint32`. + * + * The timestamps are encoded in the offset/diff format, e.g.: 1234567890123,1000,1000,1000. + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated uint64 timestamp = 1; + + /* + * Using `sint` as the one-axial-sensor-value can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * + * The axial sensor value is encoded in the offset/diff format, e.g.: + * (-0.009 m/s^2, +0.359 m/s^2, -4.82 m/s^2) => (-9 mm/s^2, +359 mm/s^2, -5_179 mm/s^2) + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * The sensor value in mm/s^2, which can also be negative (as absolute number, beside the diff-annotation used here). + * The sensor value is rounded to the closest mm/s^2 value, "with ties rounding to positive infinity" as stated in the Java doc. + * + * Absolute values between +-16 m/s^2 are expected, i.e. +- 32_000 mm/s^2 diff. + * `32 bit` i.e. we support diffs up to ~2*10^9 mm/s^2 ~ 2.000 km/s^2. We should be fine here. + */ + repeated sint32 x = 2; + repeated sint32 y = 3; + repeated sint32 z = 4; +} + +/* + * A message type which wraps all rotation points captured for one measurement. + * + * Each field contains the ordered list of that attribute for all points. + * I.e. field1[4] and field2[4] belong to the same point. + * + * Reference phone, Pixel 3a, contains this sensor: https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi160/ + * - Bosch recommends the following sensor: https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/ + * - the sensitivity and value ranges are the same [DAT-646] + * - the sensitivity is 0.0010652635 rad/s. (e.g. the Tinkerforge IMU uses 1/16 °/s = 0.00109083078 rad/s) + * - not sure how to get the sensitivity from the `262.4 LSB/°/s` from the data sheet, though + * - Thus, we choose a unit value of 0.001 rad/s, i.e 1 rad/1000s (which would be 3.6 rad/h, but we don't use that unit). + */ +message Rotations { + /* + * Using `uint` as we expect the timestamps to be in order, i.e. usually only ascending. + * Negative numbers are encoded inefficiently in this data type, compared to `sint32`. + * + * The timestamps are encoded in the offset/diff format, e.g.: 12345678901230,10000,10000,10000. + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated uint64 timestamp = 1; + + /* + * Using `sint` as the one-axial-sensor-value can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * + * The axial sensor value is encoded in the offset/diff format, e.g.: + * (+0.083 rad/s, -0.051 rad/s, +4.367 rad/s) => (83 rad/1000s, -134 rad/1000s, +4_418 rad/1000s) + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * The sensor value in rad/1000s, which can also be negative (as absolute number, beside the diff-annotation used here). + * The sensor value is rounded to the closest rad/1000s value, "with ties rounding to positive infinity" as stated in the Java doc. + * + * Absolute values between +(-?) 2000 degrees/s are expected, i.e. +- 2*34.906585 rad/s, i.e. +-69_814 rad/1000s diff. + * `32 bit` i.e. we support diffs up to ~2*10^9 rad/1000s ~ 2*10^6 rad/s. We should be fine here. + */ + repeated sint32 x = 2; + repeated sint32 y = 3; + repeated sint32 z = 4; +} + +/* + * A message type which wraps all direction points captured for one measurement. + * + * Each field contains the ordered list of that attribute for all points. + * I.e. field1[4] and field2[4] belong to the same point. + * + * Reference phone, Pixel 3a, contains this sensor: akm, AK0991X Magnetometer, Version 20012 (no good data sheet found) + * - Another phones use https://www.digikey.de/catalog/en/partgroup/ak09915/65408, with 1/16 resolution (same as e.g. Tinkerforge) + * - the sensitivity is 0.15 µT. (e.g. Tinkerforge IMU uses 1/16 µT = 0.0625 µT) + * - Thus, we choose a unit value of 0.01 µT, i.e 1 µT/100 (which would be 10 nT, but we don't use that unit). + */ +message Directions { + /* + * Using `uint` as we expect the timestamps to be in order, i.e. usually only ascending. + * Negative numbers are encoded inefficiently in this data type, compared to `sint32`. + * + * The timestamps are encoded in the offset/diff format, e.g.: 1234567890123,1000,1000,1000. + * This encodes the data more efficiently, as the `varint` encoding is used. + */ + repeated uint64 timestamp = 1; + + /* + * Using `sint` as the one-axial-sensor-value can as likely decrease as increase - using the diff/offset format here. + * Negative numbers are encoded more efficiently in this data type, compared to `int32`. + * + * The axial sensor value is encoded in the offset/diff format, e.g.: + * (+0.67 µT, -1.41 µT, +0.42 µT) => (67 µT/100, -208 µT/100, +183 µT/100) + * This encodes the data more efficiently, as the `varint` encoding is used. + * + * The sensor value in 1/100 µT, which can also be negative (as absolute number, beside the diff-annotation used here). + * The sensor value is rounded to the closest µT/100 value, "with ties rounding to positive infinity" as stated in the Java doc. + * The 10 nT unit does not cover travels between galaxies, at least you can't orientate by magnetic values due to the weak field there. + * + * Absolute values between +- 4911.994 µT on Pixel 3a and +- 1300 µT on Tinkerforge are expected, i.e. +- 982_400 µT/100 diff. + * `32 bit` i.e. we support diffs up to ~2*10^9 µT/100 ~ 20 T so strong magnet as in the LHC/CERN are covered theoretically. + */ + repeated sint32 x = 2; + repeated sint32 y = 3; + repeated sint32 z = 4; +} + +/* + * A message type which wraps data of one event captured, such as the `start` lifecycle event or a `modality change`. + * + * We store each event separately we don't expect different event types to contain all fields. + * Fields like `timestamp`, `type` and `value` use a 1 Byte field number (1-15) as this field probably occurs in most events. + * + * If we do increase the frequency later on, we should also change the field number of `Measurement.events` to a 1 Byte number. + */ +message Event { + /* + * Using `int64` as we don't expect negative timestamps. + * + * As we don't expect as many events as e.g. locations, we don't use the offset/diff variant here. + */ + uint64 timestamp = 1; + + /* + * Enum which helps to identify different types of event and to know which event fields to expect. + */ + EventType type = 2; + + /* + * Additional information. Dependent on the `EventType`. + * + * E.g.: the new value after a `MODALITY_TYPE_CHANGE` such as `BICYCLE`. + * + * Protobuf `strings` must always contain UTF-8 encoded (or 7-bit ASCII text), and cannot be longer than 2^32. + */ + string value = 3; + + /* + * Enum which helps to identify different types of event and to know which event fields to expect. + * + * The order of the enum definitions are equal to the order before the `protobuf` implementation. + * + * The 1st element must be 0. Following the Style Guide it's named enum_name_UNSPECIFIED. + * For more details and options like "alias" see: https://developers.google.com/protocol-buffers/docs/proto3#enum + */ + enum EventType { + /* + * Reserving 1-15 for events which we might want to add later which occur very frequently + * + * Our current events like lifecycle-button-presses or modality-type-changes only occur rarely + * An example for high-frequent event types could be e.g. debugging/crash events + */ + EVENT_TYPE_UNSPECIFIED = 0; // Default value + LIFECYCLE_START = 16; + LIFECYCLE_STOP = 17; + LIFECYCLE_RESUME = 18; + LIFECYCLE_PAUSE = 19; + MODALITY_TYPE_CHANGE = 20; + } +} + +/* + * Message type which wraps a file which is collected during one measurement. + * + * Reserving 1 Byte field numbers 1-15 for `repeated` complex type field which we might want to add later. + */ +message File { + /* + * The timestamp representing the "time of capturing" of the data inside the file. + * + * `uint64` used as we don't expect negative timestamps. + * As files are usually quite larges, we don't same the timestamp in the offset/diff format. + */ + uint64 timestamp = 16; + + /* + * The type of the file represented by the binary data. + */ + FileType type = 17; + + /* + * The binary data of the file. + * + * "Any arbitrary sequence of bytes no longer than 2^32" https://developers.google.com/protocol-buffers/docs/proto3#scalar + */ + bytes bytes = 18; + + /* + * The type of the file represented by the binary data. + * + * The 1st element must be 0. Following the Style Guide it's named enum_name_UNSPECIFIED. + * For more details and options like "alias" see: https://developers.google.com/protocol-buffers/docs/proto3#enum + */ + enum FileType { + FILE_TYPE_UNSPECIFIED = 0; // Default value + JPG = 1; // Compressed image, interpreted as images captured during the measurement, e.g. from Android + DNG = 2; // Raw image, interpreted as images captured during the measurement, e.g. from Android + MP4 = 3; // Video, interpreted as a video captured during the measurement, e.g. from Android + CSV = 4; // CSV used e.g. to protocol the image capturing on Android + } +} \ No newline at end of file diff --git a/src/test/java/de/cyface/protos/model/MeasurementOrBuilderTest.java b/src/test/java/de/cyface/protos/model/MeasurementOrBuilderTest.java new file mode 100644 index 0000000..cddbaee --- /dev/null +++ b/src/test/java/de/cyface/protos/model/MeasurementOrBuilderTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2021 Cyface GmbH - All Rights Reserved + * + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +package de.cyface.protos.model; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; + +import org.apache.commons.lang3.Validate; +import org.junit.jupiter.api.Test; + +import com.google.protobuf.InvalidProtocolBufferException; + +public class MeasurementOrBuilderTest { + + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + */ + private static final Integer SERIALIZED_SIZE_FORMAT_VERSION_ONLY = 3; + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + * - one location without elevation (2+8+6+6+1+4+4=31 Byte) + */ + private static final Integer SERIALIZED_SIZE_ONE_LOCATION_WITHOUT_ELEVATION = 34; + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + * - two locations without elevation (31 Byte for the first, 0+2+2+2+1+2+2=11 Byte for the second) + */ + private static final Integer SERIALIZED_SIZE_TWO_LOCATIONS_WITHOUT_ELEVATION = 44; + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + * - two locations without elevation (42 Bytes) + * - 2+2 Byte for the first, empty Elevation (isNull=true) + * - 2+4 Bytes for the second, absolute Elevation value + */ + private static final Integer SERIALIZED_SIZE_TWO_LOCATIONS_WITH_ONE_ELEVATION = 54; + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + * - first location without elevations (31 Bytes) + * - subsequent locations without elevations (+ 3599 * ~9.2 Bytes = ~33_110) + */ + private static final int[] SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITHOUT_ELEVATIONS = new int[] {32_000, 34_000}; + /** + * Number of bytes of a serialized measurement (1 Byte) which contains: + * - the format version (2 Bytes) + * - first location without elevations (31 Bytes) + * - subsequent locations without elevations (+ 3599 * ~9 Bytes = 33_110) + * - 2+4 Bytes for the first, absolute Elevation value (6 Bytes) + * - 2+2 Bytes for the second, relative Elevation value (+ 3599 * ~4.2 Bytes = ~15_115) + */ + private static final int[] SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITH_ELEVATIONS = new int[] {48_000, 49_000}; + + @Test + void test_serializedSize_forEmptyMeasurement() throws InvalidProtocolBufferException { + // Arrange + final var measurement = Measurement.newBuilder().setFormatVersion(2).build(); + Validate.isTrue(measurement.isInitialized()); + + // Act + final var serialized = measurement.toByteArray(); + + // Assert + assertThat(serialized.length, is(equalTo(SERIALIZED_SIZE_FORMAT_VERSION_ONLY))); + final var deserialized = Measurement.parseFrom(serialized); + assertThat(deserialized.getFormatVersion(), is(equalTo(measurement.getFormatVersion()))); + } + + @Test + void test_serializedSize_forOneLocation_withoutElevation() throws InvalidProtocolBufferException { + // Arrange + // noinspection SpellCheckingInspection + final var locations = LocationRecords.newBuilder() + .addTimestamp(1621582427000L) + .addLatitude(51_064590) // At "Hafenbrücke" in Dresden "Alberthafen" + .addLongitude(13_699045) + .addAccuracy(800) // 8 m + .addSpeed(1000) // 10 m/s + .build(); + final var measurement = Measurement.newBuilder() + .setFormatVersion(2) + .setLocationRecords(locations) + .build(); + Validate.isTrue(measurement.isInitialized()); + + // Act + final var serialized = measurement.toByteArray(); + + // Assert + assertThat(serialized.length, is(equalTo(SERIALIZED_SIZE_ONE_LOCATION_WITHOUT_ELEVATION))); + final var deserialized = Measurement.parseFrom(serialized); + assertThat(deserialized.getFormatVersion(), is(equalTo(measurement.getFormatVersion()))); + } + + @Test + void test_serializedSize_forTwoLocations_withoutElevations() throws InvalidProtocolBufferException { + // Arrange + // noinspection SpellCheckingInspection + final var locations = LocationRecords.newBuilder() + .addTimestamp(1621582427000L) + .addTimestamp(1000L) // 1 second later + .addLatitude(51_064590) // Crossing "Hafenbrücke" in Dresden "Alberthafen" + .addLatitude(190) + .addLongitude(13_699045) + .addLongitude(-700) + .addAccuracy(800) // 8 m + .addAccuracy(-300) // 5 m + .addSpeed(1000) // 10 m/s + .addSpeed(-1000) // 0 m/s + .build(); + final var measurement = Measurement.newBuilder() + .setFormatVersion(2) + .setLocationRecords(locations) + .build(); + Validate.isTrue(measurement.isInitialized()); + + // Act + final var serialized = measurement.toByteArray(); + + // Assert + assertThat(serialized.length, is(equalTo(SERIALIZED_SIZE_TWO_LOCATIONS_WITHOUT_ELEVATION))); + final var deserialized = Measurement.parseFrom(serialized); + assertThat(deserialized.getFormatVersion(), is(equalTo(measurement.getFormatVersion()))); + assertThat(deserialized.getLocationRecords().getElevationCount(), is(equalTo(0))); + } + + @Test + void test_serializedSize_forTwoLocations_withOneElevations() throws InvalidProtocolBufferException { + // Arrange + // noinspection SpellCheckingInspection + final var locations = LocationRecords.newBuilder() + .addTimestamp(1621582427000L) + .addTimestamp(1000L) // 1 second later + .addLatitude(51_064590) // Crossing "Hafenbrücke" in Dresden "Alberthafen" + .addLatitude(190) + .addLongitude(13_699045) + .addLongitude(-700) + .addAccuracy(800) // 8 m + .addAccuracy(-300) // 5 m + .addSpeed(1000) // 10 m/s + .addSpeed(-1000) // 0 m/s + // (!) If no elevation is present for some locations only, always set `isNull` or else elevation is 0! + .addElevation(LocationRecords.Elevation.newBuilder().setIsNull(true).build()) // no elevation value + .addElevation(LocationRecords.Elevation.newBuilder().setValue(480_00).build()) // 480 m + .build(); + final var measurement = Measurement.newBuilder() + .setFormatVersion(2) + .setLocationRecords(locations) + .build(); + Validate.isTrue(measurement.isInitialized()); + + // Act + final var serialized = measurement.toByteArray(); + + // Assert + assertThat(serialized.length, is(equalTo(SERIALIZED_SIZE_TWO_LOCATIONS_WITH_ONE_ELEVATION))); + final var deserialized = Measurement.parseFrom(serialized); + assertThat(deserialized.getFormatVersion(), is(equalTo(measurement.getFormatVersion()))); + assertThat(deserialized.getLocationRecords().getElevationCount(), is(equalTo(2))); + assertThat(deserialized.getLocationRecords().getElevation(0).getIsNull(), is(equalTo(true))); + assertThat(deserialized.getLocationRecords().getElevation(1).getIsNull(), is(equalTo(false))); + assertThat(deserialized.getLocationRecords().getElevation(1).getValue(), is(equalTo(480_00))); + } + + @Test + void test_serializedSize_one3600Locations() throws InvalidProtocolBufferException { + // Arrange + final var locationsWithoutElevations = generateLocations(3600, false); + final var locationsWithElevations = generateLocations(3600, true); + final var measurementWithoutElevations = Measurement.newBuilder() + .setFormatVersion(2) + .setLocationRecords(locationsWithoutElevations) + .build(); + final var measurementWithElevations = Measurement.newBuilder() + .setFormatVersion(2) + .setLocationRecords(locationsWithElevations) + .build(); + Validate.isTrue(measurementWithoutElevations.isInitialized()); + Validate.isTrue(measurementWithElevations.isInitialized()); + + // Act + final var serializedWithoutElevations = measurementWithoutElevations.toByteArray(); + final var serializedWithElevations = measurementWithElevations.toByteArray(); + + // Assert (byte range tested with 100.000 random generated measurements) + assertThat(serializedWithoutElevations.length, + is(greaterThan(SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITHOUT_ELEVATIONS[0]))); + assertThat(serializedWithoutElevations.length, + is(lessThan(SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITHOUT_ELEVATIONS[1]))); + assertThat(serializedWithElevations.length, + is(greaterThan(SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITH_ELEVATIONS[0]))); + assertThat(serializedWithElevations.length, + is(lessThan(SERIALIZED_SIZE_RANGE_3600_LOCATIONS_WITH_ELEVATIONS[1]))); + final var deserialized = Measurement.parseFrom(serializedWithoutElevations); + assertThat(deserialized.getFormatVersion(), is(equalTo(measurementWithoutElevations.getFormatVersion()))); + } + + @SuppressWarnings("SameParameterValue") + private LocationRecords generateLocations(@SuppressWarnings("SameParameterValue") final int amount, + final boolean withElevations) { + final var builder = LocationRecords.newBuilder(); + for (var i = 0; i < amount; i++) { + if (i == 0) { + // noinspection SpellCheckingInspection + builder.addTimestamp(1621582427000L) + .addLatitude(51_064590) // Crossing "Hafenbrücke" in Dresden "Alberthafen" + .addLongitude(13_699045) + .addAccuracy(800) // 8 m + .addSpeed(1000); // 10 m/s + if (withElevations) { + builder.addElevation(LocationRecords.Elevation.newBuilder().setValue(48000).build()); // 480 m + } + } else { + final var random = (int)(Math.random() * 20); + final var randomPlusMinus = random - 10; + builder.addTimestamp(1000L) // 1 second later + .addLatitude(randomPlusMinus * 10) + .addLongitude(randomPlusMinus * 100) + .addAccuracy(randomPlusMinus * 100) + .addSpeed(randomPlusMinus * 100); + if (withElevations) { + builder.addElevation(LocationRecords.Elevation.newBuilder().setValue(randomPlusMinus * 10).build()); + } + } + } + return builder.build(); + } +}