diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 40e55a9..b761045 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,27 +15,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 16 - uses: actions/setup-java@v1 - with: - java-version: 16 - - name: Cache SonarCloud packages - uses: actions/cache@v1 + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar + distribution: "oracle" + java-version: 17 + - name: Cache Gradle packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle + - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonarqube --info + + - name: Build + run: ./gradlew compileJava compileTestJava + + - name: Test + run: ./gradlew test diff --git a/.gitignore b/.gitignore index b55c818..95ddd25 100644 --- a/.gitignore +++ b/.gitignore @@ -192,4 +192,6 @@ flycheck_*.el !gradle-wrapper.jar -node_modules/ \ No newline at end of file +node_modules/ + +.idea \ No newline at end of file diff --git a/.idea/$PRODUCT_WORKSPACE_FILE$ b/.idea/$PRODUCT_WORKSPACE_FILE$ deleted file mode 100644 index 96ce66b..0000000 --- a/.idea/$PRODUCT_WORKSPACE_FILE$ +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 5c98b42..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 780c8da..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -telraam \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 659bf43..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index f37c315..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/telraam_dev - - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite::memory: - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/telraam_dev - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index b3e9cbd..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml deleted file mode 100644 index d08f400..0000000 --- a/.idea/jpa-buddy.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 240ddf3..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__build_.xml b/.idea/runConfigurations/Telraam__build_.xml deleted file mode 100644 index 0d73339..0000000 --- a/.idea/runConfigurations/Telraam__build_.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - true - true - false - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__migrateDevelopmentDatabase_.xml b/.idea/runConfigurations/Telraam__migrateDevelopmentDatabase_.xml deleted file mode 100644 index 202127f..0000000 --- a/.idea/runConfigurations/Telraam__migrateDevelopmentDatabase_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__migrateProductionDatabase_.xml b/.idea/runConfigurations/Telraam__migrateProductionDatabase_.xml deleted file mode 100644 index ffc82b4..0000000 --- a/.idea/runConfigurations/Telraam__migrateProductionDatabase_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__migrateTestingDatabase_.xml b/.idea/runConfigurations/Telraam__migrateTestingDatabase_.xml deleted file mode 100644 index b2859c2..0000000 --- a/.idea/runConfigurations/Telraam__migrateTestingDatabase_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__runDev_.xml b/.idea/runConfigurations/Telraam__runDev_.xml deleted file mode 100644 index c3766a2..0000000 --- a/.idea/runConfigurations/Telraam__runDev_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__runProd_.xml b/.idea/runConfigurations/Telraam__runProd_.xml deleted file mode 100644 index 0f6b693..0000000 --- a/.idea/runConfigurations/Telraam__runProd_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__test_.xml b/.idea/runConfigurations/Telraam__test_.xml deleted file mode 100644 index fdffce2..0000000 --- a/.idea/runConfigurations/Telraam__test_.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Telraam__test_force_.xml b/.idea/runConfigurations/Telraam__test_force_.xml deleted file mode 100644 index f18cffe..0000000 --- a/.idea/runConfigurations/Telraam__test_force_.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/sonarlint-state.xml b/.idea/sonarlint-state.xml deleted file mode 100644 index 0b9835d..0000000 --- a/.idea/sonarlint-state.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - 1634050067000 - - \ No newline at end of file diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml deleted file mode 100644 index 4b4f6a7..0000000 --- a/.idea/sonarlint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml deleted file mode 100644 index 0840fc3..0000000 --- a/.idea/sqldialects.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5269053..6dd0107 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,27 @@ import org.flywaydb.gradle.task.FlywayMigrateTask +buildscript { + dependencies { + classpath("org.flywaydb:flyway-database-postgresql:10.10.0") + } +} + plugins { id 'java' id 'application' - id 'jacoco' - id 'org.sonarqube' version "3.0" id 'idea' - id 'org.flywaydb.flyway' version "8.0.0" + id 'org.flywaydb.flyway' version "10.0.0" } group 'telraam' version '1.0-SNAPSHOT' -sourceCompatibility = 16 +sourceCompatibility = 17 // Set our project variables project.ext { - dropwizardVersion = '2.0.25' + dropwizardVersion = '4.0.5' + jettyVersion = '11.0.19' } repositories { @@ -26,7 +31,7 @@ application { mainClass.set('telraam.App') } -task runDev { +tasks.register('runDev') { finalizedBy { run.environment("CONFIG_KEY", "DEVELOPMENT") @@ -34,7 +39,7 @@ task runDev { run } } -task runProd { +tasks.register('runProd') { finalizedBy { run.environment("CONFIG_KEY", "PRODUCTION") @@ -47,7 +52,9 @@ idea { inheritOutputDirs = true } } -build.finalizedBy(javadoc) +build { + finalizedBy(javadoc) +} dependencies { // Web framework stuff @@ -56,26 +63,41 @@ dependencies { 'io.dropwizard:dropwizard-hibernate:' + dropwizardVersion, 'io.dropwizard:dropwizard-auth:' + dropwizardVersion, 'io.dropwizard:dropwizard-jdbi3:' + dropwizardVersion, + 'org.eclipse.jetty.websocket:websocket-jetty-api:' + jettyVersion, + 'org.eclipse.jetty.websocket:websocket-jetty-server:' + jettyVersion, ) + + // Websocket client libs + compileOnly 'jakarta.websocket:jakarta.websocket-client-api:2.2.0-M1' + // Impl for jakarta websocket clients + implementation 'org.eclipse.jetty.websocket:websocket-jakarta-client:11.0.20' + // Database - implementation('com.h2database:h2:1.4.200') - implementation('org.postgresql:postgresql:42.2.24.jre7') + implementation('com.h2database:h2:2.2.220') + implementation('org.postgresql:postgresql:42.7.3') // Testing - testImplementation('org.junit.jupiter:junit-jupiter:5.8.1') - testImplementation('org.flywaydb:flyway-core:7.14.1') - testImplementation('org.mockito:mockito-core:3.12.4') + testImplementation('org.junit.jupiter:junit-jupiter:5.10.2') + testImplementation('org.flywaydb:flyway-core:10.10.0') + testImplementation('org.mockito:mockito-core:5.11.0') testImplementation("io.dropwizard:dropwizard-testing:" + dropwizardVersion) // Statistics for Viterbi-lapper - implementation("org.apache.commons:commons-math3:3.0") + implementation("org.apache.commons:commons-math3:3.6.1") // JAX-B dependencies for JDK 9+ -> https://stackoverflow.com/a/43574427 - implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' - implementation 'org.glassfish.jaxb:jaxb-runtime:3.0.1' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.5' // Swagger-UI - implementation 'com.smoketurner:dropwizard-swagger:2.0.0-1' + implementation('com.smoketurner:dropwizard-swagger:4.0.5-1') + + // Getter & Setter via annotations + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + + testCompileOnly 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' } test { @@ -84,51 +106,13 @@ test { testLogging { events "passed", "skipped", "failed" } - finalizedBy { - jacocoTestReport - } } -jacoco { - toolVersion = "0.8.7" - reportsDirectory = layout.buildDirectory.dir('coverage').get() -} -jacocoTestReport { - dependsOn { - test - } - reports { - xml.required = true - } - afterEvaluate { - classDirectories.setFrom files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - '**/database/models/**' - ]) - }) - } -} -jacocoTestCoverageVerification { - afterEvaluate { - classDirectories.setFrom files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - '**/database/models/**' - ]) - }) - } - violationRules { - rule { - limit { - minimum = 0.7 - } - } - } -} def prodProps = new Properties() file("$rootProject.projectDir/src/main/resources/telraam/prodConfig.properties").withInputStream { prodProps.load(it) } -task migrateProductionDatabase(type: FlywayMigrateTask) { +tasks.register('migrateProductionDatabase', FlywayMigrateTask) { url = prodProps.getProperty("DB_URL") } @@ -136,7 +120,7 @@ def devProps = new Properties() file("$rootProject.projectDir/src/main/resources/telraam/devConfig.properties").withInputStream { devProps.load(it) } -task migrateDevelopmentDatabase(type: FlywayMigrateTask) { +tasks.register('migrateDevelopmentDatabase', FlywayMigrateTask) { url = devProps.getProperty("DB_URL") user = devProps.getProperty("DB_USER") password = devProps.getProperty("DB_PASSWORD") @@ -146,14 +130,7 @@ def testProps = new Properties() file("$rootProject.projectDir/src/test/resources/telraam/testConfig.properties").withInputStream { testProps.load(it) } -task migrateTestingDatabase(type: FlywayMigrateTask) { +tasks.register('migrateTestingDatabase', FlywayMigrateTask) { url = testProps.getProperty("DB_URL") baselineOnMigrate = true } -sonarqube { - properties { - property "sonar.projectKey", "12urenloop_Telraam" - property "sonar.organization", "12urenloop" - property "sonar.host.url", "https://sonarcloud.io" - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..a80b22c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,99 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # 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 +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac 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"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # 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 - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +130,120 @@ 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. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + 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 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 +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac 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 +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # 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 +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # 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\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg 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" +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..7101f8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 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 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle index fee0901..0c809d4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ rootProject.name = 'telraam' - diff --git a/src/main/java/telraam/App.java b/src/main/java/telraam/App.java index 6f02ab9..7b8cc07 100644 --- a/src/main/java/telraam/App.java +++ b/src/main/java/telraam/App.java @@ -1,39 +1,54 @@ package telraam; -import io.dropwizard.Application; +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import io.dropwizard.jdbi3.JdbiFactory; import io.dropwizard.jdbi3.bundles.JdbiExceptionsBundle; import io.dropwizard.jersey.setup.JerseyEnvironment; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; import io.federecio.dropwizard.swagger.SwaggerBundle; import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import lombok.Getter; +import lombok.Setter; import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.jdbi.v3.core.Jdbi; import telraam.api.*; import telraam.database.daos.*; import telraam.database.models.Station; import telraam.healthchecks.TemplateHealthCheck; -import telraam.logic.Lapper; -import telraam.logic.external.ExternalLapper; -import telraam.logic.robustLapper.RobustLapper; -import telraam.logic.viterbi.ViterbiLapper; -import telraam.station.Fetcher; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.external.ExternalLapper; +import telraam.logic.lapper.robust.RobustLapper; +import telraam.logic.lapper.slapper.Slapper; +import telraam.logic.positioner.Positioner; +import telraam.logic.positioner.Stationary.Stationary; +import telraam.logic.positioner.nostradamus.v2.Nostradamus; +import telraam.logic.positioner.nostradamus.v1.NostradamusV1; +import telraam.station.FetcherFactory; import telraam.util.AcceptedLapsUtil; +import telraam.websocket.WebSocketConnection; -import javax.servlet.DispatcherType; -import javax.servlet.FilterRegistration; -import java.io.IOException; import java.util.EnumSet; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; public class App extends Application { - private static Logger logger = Logger.getLogger(App.class.getName()); + private static final Logger logger = Logger.getLogger(App.class.getName()); + + @Getter private AppConfiguration config; + + @Getter private Environment environment; + + @Getter private Jdbi database; + + @Setter private boolean testing; public static void main(String[] args) throws Exception { @@ -46,10 +61,6 @@ public App() { testing = true; } - public void setTesting(boolean testing) { - this.testing = testing; - } - @Override public String getName() { return "hello-world"; @@ -69,7 +80,7 @@ protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(AppConfigurat } @Override - public void run(AppConfiguration configuration, Environment environment) throws IOException { + public void run(AppConfiguration configuration, Environment environment) { this.config = configuration; this.environment = environment; // Add database @@ -79,6 +90,15 @@ public void run(AppConfiguration configuration, Environment environment) throws // Initialize AcceptedLapUtil AcceptedLapsUtil.createInstance(this.database); + // Register websocket endpoint + JettyWebSocketServletContainerInitializer.configure( + environment.getApplicationContext(), + (servletContext, wsContainer) -> { + wsContainer.setMaxTextMessageSize(65535); + wsContainer.addMapping("/ws", (req, res) -> new WebSocketConnection()); + } + ); + // Add api resources JerseyEnvironment jersey = environment.jersey(); jersey.register(new BatonResource(database.onDemand(BatonDAO.class))); @@ -87,14 +107,15 @@ public void run(AppConfiguration configuration, Environment environment) throws jersey.register(new LapResource(database.onDemand(LapDAO.class))); jersey.register(new TeamResource(database.onDemand(TeamDAO.class), database.onDemand(BatonSwitchoverDAO.class))); jersey.register(new LapSourceResource(database.onDemand(LapSourceDAO.class))); + jersey.register(new PositionSourceResource(database.onDemand(PositionSourceDAO.class))); jersey.register(new BatonSwitchoverResource(database.onDemand(BatonSwitchoverDAO.class))); jersey.register(new LapSourceSwitchoverResource(database.onDemand(LapSourceSwitchoverDAO.class))); jersey.register(new AcceptedLapsResource()); jersey.register(new TimeResource()); - jersey.register(new LapCountResource(database.onDemand(TeamDAO.class))); + jersey.register(new LapCountResource(database.onDemand(TeamDAO.class), database.onDemand(LapDAO.class))); + jersey.register(new MonitoringResource(database)); environment.healthChecks().register("template", new TemplateHealthCheck(configuration.getTemplate())); - // Enable CORS final FilterRegistration.Dynamic cors = environment.servlets().addFilter("CORS", CrossOriginFilter.class); @@ -106,40 +127,34 @@ public void run(AppConfiguration configuration, Environment environment) throws // Add URL mapping cors.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); - if (! testing) { + if (!testing) { // Set up lapper algorithms Set lappers = new HashSet<>(); - // Old viterbi lapper is disabled - //lappers.add(new ViterbiLapper(this.database)); - lappers.add(new ExternalLapper(this.database)); lappers.add(new RobustLapper(this.database)); + lappers.add(new Slapper(this.database)); // Enable lapper APIs for (Lapper lapper : lappers) { lapper.registerAPI(jersey); } + // Set up positioners + Set positioners = new HashSet<>(); + + positioners.add(new Stationary(this.database)); + positioners.add(new NostradamusV1(this.database)); + positioners.add(new Nostradamus(configuration, this.database)); + // Start fetch thread for each station + FetcherFactory fetcherFactory = new FetcherFactory(this.database, lappers, positioners); StationDAO stationDAO = this.database.onDemand(StationDAO.class); for (Station station : stationDAO.getAll()) { - new Thread(() -> new Fetcher(this.database, station, lappers).fetch()).start(); + new Thread(() -> fetcherFactory.create(station).fetch()).start(); } } logger.info("Up and running!"); } - - public AppConfiguration getConfig() { - return config; - } - - public Environment getEnvironment() { - return environment; - } - - public Jdbi getDatabase() { - return database; - } } diff --git a/src/main/java/telraam/AppConfiguration.java b/src/main/java/telraam/AppConfiguration.java index 3a45777..9648e63 100644 --- a/src/main/java/telraam/AppConfiguration.java +++ b/src/main/java/telraam/AppConfiguration.java @@ -1,59 +1,34 @@ package telraam; import com.fasterxml.jackson.annotation.JsonProperty; -import io.dropwizard.Configuration; +import io.dropwizard.core.Configuration; import io.dropwizard.db.DataSourceFactory; import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration; -import telraam.api.responses.Template; - -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; public class AppConfiguration extends Configuration { @NotNull + @Getter @Setter private String template; @NotNull + @Getter @Setter private String defaultName = "Stranger"; + @JsonProperty("swagger") + public SwaggerBundleConfiguration swaggerBundleConfiguration; + @Valid @NotNull - private DataSourceFactory database = new DataSourceFactory(); - - @JsonProperty - public String getTemplate() { - return template; - } - - @JsonProperty - public void setTemplate(String template) { - this.template = template; - } - - public Template buildTemplate() { - return new Template(template, defaultName); - } - - @JsonProperty - public String getDefaultName() { - return defaultName; - } - - @JsonProperty - public void setDefaultName(String name) { - this.defaultName = name; - } - - @JsonProperty("database") - public DataSourceFactory getDataSourceFactory() { - return database; - } - + @Getter @Setter @JsonProperty("database") - public void setDataSourceFactory(DataSourceFactory factory) { - this.database = factory; - } + private DataSourceFactory dataSourceFactory = new DataSourceFactory(); - @JsonProperty("swagger") - public SwaggerBundleConfiguration swaggerBundleConfiguration; + @NotNull + @Getter + @JsonProperty("finish_offset") + private int finishOffset; } diff --git a/src/main/java/telraam/api/AbstractListableResource.java b/src/main/java/telraam/api/AbstractListableResource.java index e600a1c..b9e5030 100644 --- a/src/main/java/telraam/api/AbstractListableResource.java +++ b/src/main/java/telraam/api/AbstractListableResource.java @@ -1,6 +1,7 @@ package telraam.api; +import io.swagger.v3.oas.annotations.Operation; import telraam.database.daos.DAO; import java.util.List; @@ -11,6 +12,7 @@ protected AbstractListableResource(DAO dao) { } @Override + @Operation(summary = "Find all") public List getListOf() { return dao.getAll(); } diff --git a/src/main/java/telraam/api/AbstractResource.java b/src/main/java/telraam/api/AbstractResource.java index ce0e097..62f9bd7 100644 --- a/src/main/java/telraam/api/AbstractResource.java +++ b/src/main/java/telraam/api/AbstractResource.java @@ -1,12 +1,15 @@ package telraam.api; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.checkerframework.checker.units.qual.C; import telraam.database.daos.DAO; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; import java.util.Optional; public abstract class AbstractResource implements Resource { @@ -18,17 +21,17 @@ protected AbstractResource(DAO dao) { } @Override + @Operation(summary = "Add new to the database") // TODO Validate model and return 405 for wrong input - public int create(@ApiParam(required = true) T t) { + public int create(@Parameter(required = true) T t) { return dao.insert(t); } @Override - @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid or no ID supplied"), // TODO validate ID, return 400 on wrong ID format - @ApiResponse(code = 404, message = "Entity with specified ID not found") - }) - public T get(@ApiParam(value = "ID of entity that needs to be fetched", required = true) Optional id) { + @Operation(summary = "Find by ID") + @ApiResponse(responseCode = "400", description = "Invalid or no ID supplied") // TODO validate ID, return 400 on wrong ID format + @ApiResponse(responseCode = "404", description = "Entity with specified ID not found") + public T get(@Parameter(description = "ID of entity that needs to be fetched", required = true) Optional id) { if (id.isPresent()) { Optional optional = dao.getById(id.get()); if (optional.isPresent()) { @@ -42,12 +45,12 @@ public T get(@ApiParam(value = "ID of entity that needs to be fetched", required } @Override - @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid or no ID supplied"), // TODO validate ID, return 400 on wrong ID format - @ApiResponse(code = 404, message = "Entity with specified ID not found"), - @ApiResponse(code = 405, message = "Validation exception")}) // TODO validate input, 405 on wrong input - public T update(@ApiParam(value = "Entity object that needs to be updated in the database", required = true) T t, - @ApiParam(value = "ID of entity that needs to be fetched", required = true) Optional id) { + @Operation(summary = "Update an existing") + @ApiResponse(responseCode = "400", description = "Invalid or no ID supplied") // TODO validate ID, return 400 on wrong ID format + @ApiResponse(responseCode = "404", description = "Entity with specified ID not found") + @ApiResponse(responseCode = "405", description = "Validation exception") // TODO validate input, 405 on wrong input + public T update(@Parameter(description = "Entity object that needs to be updated in the database", required = true) T t, + @Parameter(description = "ID of entity that needs to be fetched", required = true) Optional id) { if (id.isPresent()) { Optional optionalBaton = dao.getById(id.get()); if (optionalBaton.isPresent()) { @@ -62,11 +65,12 @@ public T update(@ApiParam(value = "Entity object that needs to be updated in the } @Override + @Operation(summary = "Delete an existing") @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid or no ID supplied"), // TODO validate ID, return 400 on wrong ID format + @ApiResponse(responseCode = "400", description = "Invalid or no ID supplied"), // TODO validate ID, return 400 on wrong ID format }) public boolean delete( - @ApiParam(value = "ID of entity that needs to be deleted", required = true) Optional id) { + @Parameter(description = "ID of entity that needs to be deleted", required = true) Optional id) { if (id.isPresent()) { return dao.deleteById(id.get()) == 1; } else { diff --git a/src/main/java/telraam/api/AcceptedLapsResource.java b/src/main/java/telraam/api/AcceptedLapsResource.java index 6b14dc4..b413511 100644 --- a/src/main/java/telraam/api/AcceptedLapsResource.java +++ b/src/main/java/telraam/api/AcceptedLapsResource.java @@ -1,27 +1,22 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import telraam.database.daos.LapDAO; -import telraam.database.daos.LapSourceSwitchoverDAO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.models.Lap; -import telraam.database.models.LapSourceSwitchover; import telraam.util.AcceptedLapsUtil; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; @Path("/accepted-laps") -@Api("/accepted-laps") +@Tag(name="Accpted Laps") @Produces(MediaType.APPLICATION_JSON) public class AcceptedLapsResource { @GET - @ApiOperation("Get all accepted laps") + @Operation(summary = "Get all accepted laps") public List getLaps() { return AcceptedLapsUtil.getInstance().getAcceptedLaps(); } diff --git a/src/main/java/telraam/api/BatonResource.java b/src/main/java/telraam/api/BatonResource.java index a584432..faa802b 100644 --- a/src/main/java/telraam/api/BatonResource.java +++ b/src/main/java/telraam/api/BatonResource.java @@ -1,50 +1,18 @@ package telraam.api; -import io.swagger.annotations.*; -import telraam.database.daos.BatonDAO; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import telraam.database.daos.DAO; import telraam.database.models.Baton; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; -import java.util.List; -import java.util.Optional; - @Path("/baton") // dropwizard -@Api(value = "/baton") // Swagger +@Tag(name = "Baton") @Produces(MediaType.APPLICATION_JSON) public class BatonResource extends AbstractListableResource { - public BatonResource(BatonDAO dao) { + public BatonResource(DAO dao) { super(dao); } - - @Override - @ApiOperation(value = "Find all batons") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Add a new baton to the database") - public int create(Baton baton) { - return super.create(baton); - } - - @Override - @ApiOperation(value = "Find baton by ID") - public Baton get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing baton") - public Baton update(Baton baton, Optional id) { - return super.update(baton, id); - } - - @Override - @ApiOperation(value = "Delete an existing baton") - public boolean delete(Optional id) { - return super.delete(id); - } } diff --git a/src/main/java/telraam/api/BatonSwitchoverResource.java b/src/main/java/telraam/api/BatonSwitchoverResource.java index 6cc6611..9b1a173 100644 --- a/src/main/java/telraam/api/BatonSwitchoverResource.java +++ b/src/main/java/telraam/api/BatonSwitchoverResource.java @@ -1,43 +1,18 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import telraam.database.daos.BatonDAO; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.BatonSwitchoverDAO; -import telraam.database.models.Baton; import telraam.database.models.BatonSwitchover; -import telraam.database.models.LapSourceSwitchover; - -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; -import java.util.Optional; @Path("/batonswitchover") // dropwizard -@Api(value = "/batonswitchover") // Swagger +@Tag(name = "Baton Switchover") @Produces(MediaType.APPLICATION_JSON) public class BatonSwitchoverResource extends AbstractListableResource { public BatonSwitchoverResource(BatonSwitchoverDAO dao) { super(dao); } - - @Override - @ApiOperation(value = "Find all baton switchovers") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Find baton switchover by ID") - public BatonSwitchover get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Add a new baton switchover to the database") - public int create(BatonSwitchover batonSwitchover) { - return super.create(batonSwitchover); - } } diff --git a/src/main/java/telraam/api/DetectionResource.java b/src/main/java/telraam/api/DetectionResource.java index 6ab0f35..b7566cc 100644 --- a/src/main/java/telraam/api/DetectionResource.java +++ b/src/main/java/telraam/api/DetectionResource.java @@ -1,18 +1,18 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.DetectionDAO; import telraam.database.models.Detection; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; import java.util.List; import java.util.Optional; @Path("/detection") -@Api(value = "/detection") // Swagger @Produces(MediaType.APPLICATION_JSON) +@Tag(name="Detection") public class DetectionResource extends AbstractListableResource { private final DetectionDAO detectionDAO; @@ -22,39 +22,9 @@ public DetectionResource(DetectionDAO dao) { detectionDAO = dao; } - @Override - @ApiOperation(value = "Find all detections") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Add a new detection to the database") - public int create(Detection detection) { - return super.create(detection); - } - - @Override - @ApiOperation(value = "Find detection by ID") - public Detection get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing detection") - public Detection update(Detection detection, Optional id) { - return super.update(detection, id); - } - - @Override - @ApiOperation(value = "Delete an existing detection") - public boolean delete(Optional id) { - return super.delete(id); - } - @GET @Path("/since/{id}") - @ApiOperation(value = "Get detections with ID larger than given ID") + @Operation(summary = "Get detections with ID larger than given ID") public List getListSince(@PathParam("id") Integer id, @QueryParam("limit") Optional limit) { return detectionDAO.getSinceId(id, limit.orElse(1000)); } diff --git a/src/main/java/telraam/api/LapCountResource.java b/src/main/java/telraam/api/LapCountResource.java index c3a857d..edb7b03 100644 --- a/src/main/java/telraam/api/LapCountResource.java +++ b/src/main/java/telraam/api/LapCountResource.java @@ -1,32 +1,37 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; import telraam.database.daos.LapDAO; -import telraam.database.daos.LapSourceSwitchoverDAO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.TeamDAO; import telraam.database.models.Lap; -import telraam.database.models.LapSourceSwitchover; +import telraam.database.models.LapCount; import telraam.database.models.Team; import telraam.util.AcceptedLapsUtil; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; @Path("/lap-counts") -@Api("/lap-counts") +@Tag(name="Lap Counts") @Produces(MediaType.APPLICATION_JSON) public class LapCountResource { - TeamDAO teamDAO; + private TeamDAO teamDAO; + private LapDAO lapDAO; - public LapCountResource(TeamDAO teamDAO) { + public LapCountResource(TeamDAO teamDAO, LapDAO lapDAO) { this.teamDAO = teamDAO; + this.lapDAO = lapDAO; } + @GET - @ApiOperation("Get the current lap counts per team") + @Operation(summary = "Get the current lap counts per team") public Map getLapCounts() { Map perId = new HashMap<>(); for (Lap lap : AcceptedLapsUtil.getInstance().getAcceptedLaps()) { @@ -55,4 +60,27 @@ public Map getLapCounts() { return perName; } + + @GET + @Path("/{lapSourceId}") + public List getLapCountForLapSource(@PathParam("lapSourceId") Integer id, @QueryParam("end") Optional endTimestamp) { + LocalDateTime dateTime = LocalDateTime.now(); + if (endTimestamp.isPresent()) { + dateTime = LocalDateTime.parse(endTimestamp.get()); + } + List laps = lapDAO.getAllBeforeTime(id, Timestamp.valueOf(dateTime)); + return laps; + } + + // EndTimestamp should be a ISO formatted date timestamp + @GET + @Path("/{lapSourceId}/{teamId}") + public Integer getLapCountForLapSource(@PathParam("lapSourceId") Integer id, @PathParam("teamId") Integer teamId, @QueryParam("end") Optional endTimestamp) { + LocalDateTime dateTime = LocalDateTime.now(); + if (endTimestamp.isPresent()) { + dateTime = LocalDateTime.parse(endTimestamp.get()); + } + LapCount lapInfo = lapDAO.getAllForTeamBeforeTime(id, teamId, Timestamp.valueOf(dateTime)); + return lapInfo.getCount(); + } } diff --git a/src/main/java/telraam/api/LapResource.java b/src/main/java/telraam/api/LapResource.java index 29186e9..a0ef3db 100644 --- a/src/main/java/telraam/api/LapResource.java +++ b/src/main/java/telraam/api/LapResource.java @@ -1,20 +1,17 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.LapDAO; import telraam.database.models.Lap; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; import java.util.List; import java.util.Optional; @Path("/lap") -@Api("/lap") +@Tag(name="Lap") @Produces(MediaType.APPLICATION_JSON) public class LapResource extends AbstractResource { private final LapDAO lapDAO; @@ -25,7 +22,7 @@ public LapResource(LapDAO dao) { } @GET - @ApiOperation(value = "Find all laps") + @Operation(summary = "Find all laps") public List getListOf(@QueryParam("source") final Integer source) { if (source == null) { return lapDAO.getAll(); @@ -33,28 +30,4 @@ public List getListOf(@QueryParam("source") final Integer source) { return lapDAO.getAllBySource(source); } } - - @Override - @ApiOperation(value = "Add a new lap to the database") - public int create(Lap lap) { - return super.create(lap); - } - - @Override - @ApiOperation(value = "Find lap by ID") - public Lap get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing lap") - public Lap update(Lap lap, Optional id) { - return super.update(lap, id); - } - - @Override - @ApiOperation(value = "Delete an existing lap") - public boolean delete(Optional id) { - return super.delete(id); - } } diff --git a/src/main/java/telraam/api/LapSourceResource.java b/src/main/java/telraam/api/LapSourceResource.java index e2037e3..630e6db 100644 --- a/src/main/java/telraam/api/LapSourceResource.java +++ b/src/main/java/telraam/api/LapSourceResource.java @@ -1,51 +1,17 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.DAO; import telraam.database.models.LapSource; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; -import java.util.Optional; - @Path("/lap-source") -@Api("/lap-source") +@Tag(name = "Lap Source") @Produces(MediaType.APPLICATION_JSON) public class LapSourceResource extends AbstractListableResource { public LapSourceResource(DAO dao) { super(dao); } - - @Override - @ApiOperation(value = "Find all lap sources") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Add a new lap source to the database") - public int create(LapSource lapSource) { - return super.create(lapSource); - } - - @Override - @ApiOperation(value = "Find lap source by ID") - public LapSource get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing lap source") - public LapSource update(LapSource lapSource, Optional id) { - return super.update(lapSource, id); - } - - @Override - @ApiOperation(value = "Delete an existing lap source") - public boolean delete(Optional id) { - return super.delete(id); - } } diff --git a/src/main/java/telraam/api/LapSourceSwitchoverResource.java b/src/main/java/telraam/api/LapSourceSwitchoverResource.java index 06290bf..598cdf0 100644 --- a/src/main/java/telraam/api/LapSourceSwitchoverResource.java +++ b/src/main/java/telraam/api/LapSourceSwitchoverResource.java @@ -1,41 +1,18 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.LapSourceSwitchoverDAO; -import telraam.database.models.LapSource; import telraam.database.models.LapSourceSwitchover; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; -import java.util.Optional; - @Path("/lapsourceswitchover") // dropwizard -@Api(value = "/lapsourceswitchover") // Swagger +@Tag(name = "Lap Source Switchover") @Produces(MediaType.APPLICATION_JSON) public class LapSourceSwitchoverResource extends AbstractListableResource { public LapSourceSwitchoverResource(LapSourceSwitchoverDAO dao) { super(dao); } - - @Override - @ApiOperation(value = "Find all lap source switchovers") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Find lap source switchover by ID") - public LapSourceSwitchover get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Add a new lap source switchover to the database") - public int create(LapSourceSwitchover lapSourceSwitchover) { - return super.create(lapSourceSwitchover); - } } diff --git a/src/main/java/telraam/api/ListableResource.java b/src/main/java/telraam/api/ListableResource.java index e8bc2d5..24d84ea 100644 --- a/src/main/java/telraam/api/ListableResource.java +++ b/src/main/java/telraam/api/ListableResource.java @@ -1,6 +1,6 @@ package telraam.api; -import javax.ws.rs.GET; +import jakarta.ws.rs.GET; import java.util.List; /** diff --git a/src/main/java/telraam/api/MonitoringResource.java b/src/main/java/telraam/api/MonitoringResource.java new file mode 100644 index 0000000..732b01a --- /dev/null +++ b/src/main/java/telraam/api/MonitoringResource.java @@ -0,0 +1,127 @@ +package telraam.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.*; +import telraam.database.models.Lap; +import telraam.database.models.LapSource; +import telraam.database.models.Team; +import telraam.database.models.TeamLapCount; +import telraam.monitoring.BatonDetectionManager; +import telraam.monitoring.BatonStatusHolder; +import telraam.monitoring.StationDetectionManager; +import telraam.monitoring.models.BatonDetection; +import telraam.monitoring.models.BatonStatus; +import telraam.monitoring.models.LapCountForTeam; +import telraam.monitoring.models.TeamLapInfo; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.*; + +@Path("/monitoring") +@Tag(name = "Monitoring") +@Produces(MediaType.APPLICATION_JSON) +public class MonitoringResource { + private final BatonStatusHolder batonStatusHolder; + private final BatonDetectionManager batonDetectionManager; + private final StationDetectionManager stationDetectionManager; + private final TeamDAO teamDAO; + private final LapDAO lapDAO; + private final LapSourceDAO lapSourceDAO; + + public MonitoringResource(Jdbi jdbi) { + this.teamDAO = jdbi.onDemand(TeamDAO.class); + this.lapDAO = jdbi.onDemand(LapDAO.class); + this.lapSourceDAO = jdbi.onDemand(LapSourceDAO.class); + this.batonStatusHolder = new BatonStatusHolder(jdbi.onDemand(BatonDAO.class), jdbi.onDemand(DetectionDAO.class)); + this.batonDetectionManager = new BatonDetectionManager(jdbi.onDemand(DetectionDAO.class), this.teamDAO, jdbi.onDemand(BatonSwitchoverDAO.class)); + this.stationDetectionManager = new StationDetectionManager(jdbi.onDemand(DetectionDAO.class), jdbi.onDemand(StationDAO.class)); + } + + @GET + @Path("/batons") + @Operation(summary = "Get the status of all the batons, including unused batons which are toggleable via a parameter") + public List getBatonMetrics(@QueryParam("filter_assigned") boolean filterAssigned) { + List batonStatuses = batonStatusHolder.GetAllBatonStatuses(); + if (filterAssigned) { + List teams = teamDAO.getAll(); + Set usedBatonIds = new HashSet<>(); + for (Team team : teams) { + usedBatonIds.add(team.getBatonId()); + } + return batonStatuses.stream().filter(batonStatus -> usedBatonIds.contains(batonStatus.getId())).toList(); + } + return batonStatuses; + } + + @POST + @Path("/reset-rebooted/{batonId}") + @Operation(summary = "Reset the rebooted flag of a baton") + public void resetRebooted(@PathParam("batonId") Integer batonId) { + batonStatusHolder.resetRebooted(batonId); + } + + @GET + @Path("/team-detection-times") + @Operation(summary = "A map of all detections per batons") + public Map> getTeamDetectionTimes() { + return batonDetectionManager.getBatonDetections(); + } + + @GET + @Path("/stations-latest-detection-time") + @Operation(summary = "Get the map of all station name to time since last detection") + public Map getStationIDToLatestDetectionTimeMap() { + return stationDetectionManager.timeSinceLastDetectionPerStation(); + } + + @GET + @Path("/team-lap-times/{lapperId}") + @Operation(summary = "Get monitoring data that can be used as grafana datasource") + public Map> getTeamLapTimes(@PathParam("lapperId") Integer id) { + List laps = lapDAO.getAllBySourceSorted(id); + List teams = teamDAO.getAll(); + Map teamMap = new HashMap<>(); + for (Team team : teams) { + teamMap.put(team.getId(), team); + } + Map> teamLapInfos = new HashMap<>(); + Map previousLap = new HashMap<>(); + for (Lap lap : laps) { + if (!previousLap.containsKey(lap.getTeamId())) { + previousLap.put(lap.getTeamId(), lap); + continue; + } + Lap prevLap = previousLap.get(lap.getTeamId()); + previousLap.put(lap.getTeamId(), lap); + if (!teamLapInfos.containsKey(lap.getTeamId())) { + teamLapInfos.put(lap.getTeamId(), new ArrayList<>()); + } + Team team = teamMap.get(lap.getTeamId()); + teamLapInfos.get(lap.getTeamId()).add(new TeamLapInfo((lap.getTimestamp().getTime() - prevLap.getTimestamp().getTime()) / 1000, lap.getTimestamp().getTime() / 1000, lap.getTeamId(), team.getName())); + } + return teamLapInfos; + } + + @GET + @Path("/team-lap-counts") + @Operation(summary = "Get monitoring data that can be used as grafana datasource") + public List getTeamLapCounts() { + List teams = teamDAO.getAll(); + List lapSources = lapSourceDAO.getAll(); + List lapCountForTeams = new ArrayList<>(); + for (Team team : teams) { + var teamLapsCount = lapDAO.getAllBySourceAndTeam(team.getId()); + Map lapCounts = new HashMap<>(); + lapSources.forEach(lapSource -> lapCounts.put(lapSource.getId(), 0)); + for (TeamLapCount teamLapCount : teamLapsCount) { + lapCounts.put(teamLapCount.getLapSourceId(), teamLapCount.getLapCount()); + } + lapCountForTeams.add(new LapCountForTeam(team.getName(), lapCounts)); + } + return lapCountForTeams; + } +} diff --git a/src/main/java/telraam/api/PositionSourceResource.java b/src/main/java/telraam/api/PositionSourceResource.java new file mode 100644 index 0000000..f59c7bb --- /dev/null +++ b/src/main/java/telraam/api/PositionSourceResource.java @@ -0,0 +1,20 @@ +package telraam.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import telraam.database.daos.DAO; +import telraam.database.models.PositionSource; + + +import java.awt.*; + +@Path("/position-source") +@Tag(name = "Position Source") +@Produces(MediaType.APPLICATION_JSON) +public class PositionSourceResource extends AbstractListableResource { + public PositionSourceResource(DAO dao) { + super(dao); + } +} diff --git a/src/main/java/telraam/api/Resource.java b/src/main/java/telraam/api/Resource.java index 6228da0..5546cba 100644 --- a/src/main/java/telraam/api/Resource.java +++ b/src/main/java/telraam/api/Resource.java @@ -1,7 +1,8 @@ package telraam.api; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + import java.util.Optional; public interface Resource { diff --git a/src/main/java/telraam/api/StationResource.java b/src/main/java/telraam/api/StationResource.java index 58c11ed..106131c 100644 --- a/src/main/java/telraam/api/StationResource.java +++ b/src/main/java/telraam/api/StationResource.java @@ -1,51 +1,17 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.DAO; import telraam.database.models.Station; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; -import java.util.Optional; - @Path("/station") -@Api(value = "/station") // Swagger +@Tag(name = "Station") @Produces(MediaType.APPLICATION_JSON) public class StationResource extends AbstractListableResource { public StationResource(DAO dao) { super(dao); } - - @Override - @ApiOperation(value = "Find all stations") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Add a new station to the database") - public int create(Station station) { - return super.create(station); - } - - @Override - @ApiOperation(value = "Find station by ID") - public Station get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing station") - public Station update(Station station, Optional id) { - return super.update(station, id); - } - - @Override - @ApiOperation(value = "Delete an existing station") - public boolean delete(Optional id) { - return super.delete(id); - } } diff --git a/src/main/java/telraam/api/TeamResource.java b/src/main/java/telraam/api/TeamResource.java index 67c5b04..9bf36c3 100644 --- a/src/main/java/telraam/api/TeamResource.java +++ b/src/main/java/telraam/api/TeamResource.java @@ -1,15 +1,15 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.BatonSwitchoverDAO; import telraam.database.daos.TeamDAO; import telraam.database.models.BatonSwitchover; import telraam.database.models.Team; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; import java.sql.Timestamp; import java.time.Instant; import java.util.List; @@ -18,23 +18,18 @@ @Path("/team") -@Api("/team") +@Tag(name="Team") @Produces(MediaType.APPLICATION_JSON) public class TeamResource extends AbstractListableResource { BatonSwitchoverDAO batonSwitchoverDAO; + public TeamResource(TeamDAO teamDAO, BatonSwitchoverDAO batonSwitchoverDAO) { super(teamDAO); this.batonSwitchoverDAO = batonSwitchoverDAO; } @Override - @ApiOperation(value = "Find all teams") - public List getListOf() { - return super.getListOf(); - } - - @Override - @ApiOperation(value = "Add a new team to the database") + @Operation(summary = "Add a new team to the database") public int create(Team team) { int ret = super.create(team); @@ -51,35 +46,20 @@ public int create(Team team) { } @Override - @ApiOperation(value = "Find team by ID") - public Team get(Optional id) { - return super.get(id); - } - - @Override - @ApiOperation(value = "Update an existing team") + @Operation(summary = "Update an existing team") public Team update(Team team, Optional id) { Team previousTeam = this.get(id); Team ret = super.update(team, id); - System.out.println(previousTeam.getBatonId()); - System.out.println(team.getBatonId()); - if (!Objects.equals(previousTeam.getBatonId(), team.getBatonId())) { this.batonSwitchoverDAO.insert(new BatonSwitchover( - team.getId(), - previousTeam.getBatonId(), - team.getBatonId(), - Timestamp.from(Instant.now()) + team.getId(), + previousTeam.getBatonId(), + team.getBatonId(), + Timestamp.from(Instant.now()) )); } return ret; } - - @Override - @ApiOperation(value = "Delete an existing team") - public boolean delete(Optional id) { - return super.delete(id); - } } diff --git a/src/main/java/telraam/api/TimeResource.java b/src/main/java/telraam/api/TimeResource.java index 0c85770..f49e0c3 100644 --- a/src/main/java/telraam/api/TimeResource.java +++ b/src/main/java/telraam/api/TimeResource.java @@ -1,19 +1,17 @@ package telraam.api; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.time.Instant; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; @Path("/time") -@Api("/time") +@Tag(name = "Time") @Produces(MediaType.APPLICATION_JSON) public class TimeResource { - static class TimeResponse { + public static class TimeResponse { public long timestamp; public TimeResponse() { @@ -22,7 +20,7 @@ public TimeResponse() { } @GET - @ApiOperation(value = "Get current time") + @Operation(summary = "Get current time") public TimeResponse get() { return new TimeResponse(); } diff --git a/src/main/java/telraam/api/responses/Saying.java b/src/main/java/telraam/api/responses/Saying.java index 79afdf9..5750eea 100644 --- a/src/main/java/telraam/api/responses/Saying.java +++ b/src/main/java/telraam/api/responses/Saying.java @@ -1,30 +1,16 @@ package telraam.api.responses; -import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; +@Getter +@NoArgsConstructor +@AllArgsConstructor public class Saying { private long id; @Length(max = 3) private String content; - - public Saying() { - // Jackson deserialization - } - - public Saying(long id, String content) { - this.id = id; - this.content = content; - } - - @JsonProperty - public long getId() { - return id; - } - - @JsonProperty - public String getContent() { - return content; - } } \ No newline at end of file diff --git a/src/main/java/telraam/api/responses/Template.java b/src/main/java/telraam/api/responses/Template.java index ce15c84..d41829d 100644 --- a/src/main/java/telraam/api/responses/Template.java +++ b/src/main/java/telraam/api/responses/Template.java @@ -1,18 +1,16 @@ package telraam.api.responses; +import lombok.AllArgsConstructor; + import java.util.Optional; import static java.lang.String.format; +@AllArgsConstructor public class Template { private final String content; private final String defaultName; - public Template(String content, String defaultName) { - this.content = content; - this.defaultName = defaultName; - } - public String render(Optional name) { return format(content, name.orElse(defaultName)); } diff --git a/src/main/java/telraam/database/daos/BatonDAO.java b/src/main/java/telraam/database/daos/BatonDAO.java index f0859d1..2aefda9 100644 --- a/src/main/java/telraam/database/daos/BatonDAO.java +++ b/src/main/java/telraam/database/daos/BatonDAO.java @@ -28,6 +28,10 @@ public interface BatonDAO extends DAO { @RegisterBeanMapper(Baton.class) Optional getById(@Bind("id") int id); + @SqlQuery("SELECT * FROM baton WHERE mac = :mac") + @RegisterBeanMapper(Baton.class) + Optional getByMac(@Bind("mac") String mac); + @Override @SqlUpdate("DELETE FROM baton WHERE id = :id") @RegisterBeanMapper(Baton.class) diff --git a/src/main/java/telraam/database/daos/DetectionDAO.java b/src/main/java/telraam/database/daos/DetectionDAO.java index be6e6f9..982b92c 100644 --- a/src/main/java/telraam/database/daos/DetectionDAO.java +++ b/src/main/java/telraam/database/daos/DetectionDAO.java @@ -3,13 +3,13 @@ import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.BindBean; -import org.jdbi.v3.sqlobject.customizer.BindBeanList; import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlBatch; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import telraam.database.models.Detection; +import java.sql.Timestamp; import java.util.List; import java.util.Optional; @@ -33,6 +33,16 @@ INSERT INTO detection (station_id, baton_id, timestamp, rssi, battery, remote_id @GetGeneratedKeys({"id"}) int insertAll(@BindBean List detection); + @SqlBatch(""" + INSERT INTO detection (station_id, baton_id, timestamp, rssi, battery, remote_id, uptime_ms, timestamp_ingestion) + SELECT :stationId, b.id, :timestamp, :rssi, :battery, :remoteId, :uptimeMs, :timestampIngestion + FROM baton b + WHERE b.mac = :batonMac + """) + @GetGeneratedKeys({"id", "baton_id"}) + @RegisterBeanMapper(Detection.class) + List insertAllWithoutBaton(@BindBean List detection, @Bind("batonMac") List batonMac); + @SqlQuery("SELECT * FROM detection WHERE id = :id") @RegisterBeanMapper(Detection.class) Optional getById(@Bind("id") int id); @@ -55,4 +65,16 @@ INSERT INTO detection (station_id, baton_id, timestamp, rssi, battery, remote_id @SqlQuery("SELECT * FROM detection WHERE id > :id ORDER BY id LIMIT :limit") @RegisterBeanMapper(Detection.class) List getSinceId(@Bind("id") int id, @Bind("limit") int limit); + + + @SqlQuery("SELECT * FROM detection WHERE baton_id = :batonId AND timestamp >= :timestamp ORDER BY timestamp DESC LIMIT 1") + @RegisterBeanMapper(Detection.class) + Optional latestDetectionByBatonId(@Bind("batonId") int batonId, @Bind("timestamp") Timestamp timestamp); + + @SqlQuery(""" + WITH bso AS (SELECT teamid, newbatonid, timestamp AS current_timestamp, COALESCE( LEAD(timestamp) OVER (PARTITION BY teamid ORDER BY timestamp), timestamp + INTERVAL '1 year') AS next_baton_switch FROM batonswitchover) + SELECT baton_id, station_id, rssi, timestamp, teamid FROM detection d LEFT JOIN bso ON d.baton_id = bso.newbatonid AND d.timestamp BETWEEN bso.current_timestamp AND bso.next_baton_switch WHERE rssi > :minRssi + """) + @RegisterBeanMapper(Detection.class) + List getAllWithTeamId(@Bind("minRssi") int minRssi); } diff --git a/src/main/java/telraam/database/daos/LapDAO.java b/src/main/java/telraam/database/daos/LapDAO.java index ce98a1d..8afd163 100644 --- a/src/main/java/telraam/database/daos/LapDAO.java +++ b/src/main/java/telraam/database/daos/LapDAO.java @@ -3,13 +3,13 @@ import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.BindBean; -import org.jdbi.v3.sqlobject.customizer.BindBeanList; import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlBatch; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import telraam.database.models.Lap; - +import telraam.database.models.LapCount; +import telraam.database.models.TeamLapCount; import java.sql.Timestamp; import java.util.Iterator; @@ -26,6 +26,18 @@ public interface LapDAO extends DAO { @RegisterBeanMapper(Lap.class) List getAllBySource(@Bind("lapSourceId") Integer lapSourceId); + @SqlQuery("SELECT * FROM lap WHERE lap_source_id = :lapSourceId ORDER BY timestamp ASC") + @RegisterBeanMapper(Lap.class) + List getAllBySourceSorted(@Bind("lapSourceId") Integer lapSourceId); + + @SqlQuery("SELECT t.id as team_id, (SELECT COUNT(*) FROM lap WHERE lap_source_id = :lapSourceId AND timestamp <= :timestamp and team_id = t.id) as count FROM team t") + @RegisterBeanMapper(LapCount.class) + List getAllBeforeTime(@Bind("lapSourceId") Integer lapSourceId, @Bind("timestamp") Timestamp timestamp); + + @SqlQuery("SELECT t.id as team_id, (SELECT COUNT(*) FROM lap WHERE lap_source_id = :lapSourceId AND timestamp <= :timestamp and team_id = t.id) as count FROM team t where id = :teamId") + @RegisterBeanMapper(LapCount.class) + LapCount getAllForTeamBeforeTime(@Bind("lapSourceId") Integer lapSourceId, @Bind("teamId") Integer teamId, @Bind("timestamp") Timestamp timestamp); + @SqlUpdate("INSERT INTO lap (team_id, lap_source_id, timestamp) " + "VALUES (:teamId, :lapSourceId, :timestamp)") @GetGeneratedKeys({"id"}) @@ -54,6 +66,19 @@ public interface LapDAO extends DAO { @SqlUpdate("DELETE FROM lap WHERE lap_source_id = :lapSourceId") void deleteByLapSourceId(@Bind("lapSourceId") int lapSourceId); + @SqlBatch("DELETE FROM lap WHERE id = :id") + void deleteAllById(@BindBean Iterator laps); + @SqlBatch("INSERT INTO lap (team_id, lap_source_id, timestamp) VALUES (:teamId, :lapSourceId, :timestamp)") void insertAll(@BindBean Iterator laps); + + @SqlBatch("UPDATE lap SET timestamp = :timestamp WHERE id = :id") + void updateAll(@BindBean Iterator laps); + + @SqlBatch("DELETE FROM lap WHERE id = :id") + void deleteAll(@BindBean Iterator laps); + + @SqlQuery("SELECT COUNT(*) as lapCount, lap_source_id FROM lap WHERE team_id = :teamId GROUP BY lap_source_id") + @RegisterBeanMapper(TeamLapCount.class) + List getAllBySourceAndTeam(@Bind("teamId") int teamId); } diff --git a/src/main/java/telraam/database/daos/PositionSourceDAO.java b/src/main/java/telraam/database/daos/PositionSourceDAO.java new file mode 100644 index 0000000..36f172f --- /dev/null +++ b/src/main/java/telraam/database/daos/PositionSourceDAO.java @@ -0,0 +1,27 @@ +package telraam.database.daos; + +import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.BindBean; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import telraam.database.models.PositionSource; + +import java.util.List; +import java.util.Optional; + +public interface PositionSourceDAO extends DAO { + @Override + @SqlQuery("SELECT * FROM position_source") + @RegisterBeanMapper(PositionSource.class) + List getAll(); + + @SqlUpdate("INSERT INTO position_source (name) VALUES (:name)") + @GetGeneratedKeys({"id"}) + int insert(@BindBean PositionSource positionSource); + + @SqlQuery("SELECT * FROM position_source WHERE name = :name") + @RegisterBeanMapper(PositionSource.class) + Optional getByName(@Bind("name") String name); +} diff --git a/src/main/java/telraam/database/daos/StationDAO.java b/src/main/java/telraam/database/daos/StationDAO.java index cd73051..983eecf 100644 --- a/src/main/java/telraam/database/daos/StationDAO.java +++ b/src/main/java/telraam/database/daos/StationDAO.java @@ -19,7 +19,7 @@ public interface StationDAO extends DAO { List getAll(); @Override - @SqlUpdate("INSERT INTO station (name, distance_from_start, broken, url, coord_x, coord_y) VALUES (:name, :distanceFromStart, :isBroken, :url, :coordX, :coordY)") + @SqlUpdate("INSERT INTO station (name, distance_from_start, broken, url, coord_x, coord_y) VALUES (:name, :distanceFromStart, :broken, :url, :coordX, :coordY)") @GetGeneratedKeys({"id"}) int insert(@BindBean Station station); @@ -33,6 +33,6 @@ public interface StationDAO extends DAO { int deleteById(@Bind("id") int id); @Override - @SqlUpdate("UPDATE station SET name = :name, distance_from_start = :distanceFromStart, broken = :isBroken, url = :url, coord_x = :coordX, coord_y = :coordY WHERE id = :id") + @SqlUpdate("UPDATE station SET name = :name, distance_from_start = :distanceFromStart, broken = :broken, url = :url, coord_x = :coordX, coord_y = :coordY WHERE id = :id") int update(@Bind("id") int id, @BindBean Station station); } diff --git a/src/main/java/telraam/database/daos/TeamDAO.java b/src/main/java/telraam/database/daos/TeamDAO.java index e983c57..a251884 100644 --- a/src/main/java/telraam/database/daos/TeamDAO.java +++ b/src/main/java/telraam/database/daos/TeamDAO.java @@ -14,17 +14,17 @@ public interface TeamDAO extends DAO { @Override - @SqlQuery("SELECT * FROM team") + @SqlQuery("SELECT t.*, tb.baton_id FROM team t LEFT JOIN team_baton_ids tb ON tb.team_id = t.id") @RegisterBeanMapper(Team.class) List getAll(); @Override - @SqlUpdate("INSERT INTO team (name, baton_id) VALUES (:name, :batonId)") + @SqlUpdate("INSERT INTO team (name, jacket_nr) VALUES (:name, :jacketNr)") @GetGeneratedKeys({"id"}) int insert(@BindBean Team team); @Override - @SqlQuery("SELECT * FROM team WHERE id = :id") + @SqlQuery("SELECT t.*, tb.baton_id FROM team t LEFT JOIN team_baton_ids tb ON tb.team_id = t.id WHERE t.id = :id") @RegisterBeanMapper(Team.class) Optional getById(@Bind("id") int id); @@ -33,9 +33,6 @@ public interface TeamDAO extends DAO { int deleteById(@Bind("id") int id); @Override - @SqlUpdate("UPDATE team SET " + - "name = :name," + - "baton_id = :batonId " + - "WHERE id = :id") + @SqlUpdate("UPDATE team SET name = :name, jacket_nr = :jacketNr WHERE id = :id") int update(@Bind("id") int id, @BindBean Team modelObj); } diff --git a/src/main/java/telraam/database/models/Baton.java b/src/main/java/telraam/database/models/Baton.java index 525b95b..f5889c0 100644 --- a/src/main/java/telraam/database/models/Baton.java +++ b/src/main/java/telraam/database/models/Baton.java @@ -1,16 +1,17 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.util.Objects; +@Getter @Setter @NoArgsConstructor public class Baton { private Integer id; private String name; private String mac; - // DO NOT REMOVE - public Baton() { - } - public Baton(String name) { this.name = name; } @@ -20,30 +21,6 @@ public Baton(String name, String mac) { this.mac = mac; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getMac() { - return mac; - } - - public void setMac(String mac) { - this.mac = mac; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/telraam/database/models/BatonSwitchover.java b/src/main/java/telraam/database/models/BatonSwitchover.java index 67a024a..d888870 100644 --- a/src/main/java/telraam/database/models/BatonSwitchover.java +++ b/src/main/java/telraam/database/models/BatonSwitchover.java @@ -1,8 +1,13 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.sql.Timestamp; import java.util.Objects; +@Getter @Setter @NoArgsConstructor public class BatonSwitchover { private Integer id; private Integer teamId; @@ -10,9 +15,6 @@ public class BatonSwitchover { private Integer newBatonId; private Timestamp timestamp; - // DO NOT REMOVE - public BatonSwitchover() {} - public BatonSwitchover(Integer teamId, Integer previousBatonId, Integer newBatonId, Timestamp timestamp) { this.teamId = teamId; this.previousBatonId = previousBatonId; @@ -20,46 +22,6 @@ public BatonSwitchover(Integer teamId, Integer previousBatonId, Integer newBaton this.timestamp = timestamp; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Integer getTeamId() { - return teamId; - } - - public void setTeamId(Integer teamId) { - this.teamId = teamId; - } - - public Integer getPreviousBatonId() { - return previousBatonId; - } - - public void setPreviousBatonId(Integer previousBatonId) { - this.previousBatonId = previousBatonId; - } - - public Integer getNewBatonId() { - return newBatonId; - } - - public void setNewBatonId(Integer newBatonId) { - this.newBatonId = newBatonId; - } - - public Timestamp getTimestamp() { - return timestamp; - } - - public void setTimestamp(Timestamp timestamp) { - this.timestamp = timestamp; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/telraam/database/models/Detection.java b/src/main/java/telraam/database/models/Detection.java index 410167e..c00264b 100644 --- a/src/main/java/telraam/database/models/Detection.java +++ b/src/main/java/telraam/database/models/Detection.java @@ -1,7 +1,14 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.sql.Timestamp; +@Setter +@Getter +@NoArgsConstructor public class Detection { private Integer id; private Integer batonId; @@ -12,9 +19,7 @@ public class Detection { private Integer remoteId; private Timestamp timestamp; private Timestamp timestampIngestion; - - public Detection() { - } + private Integer teamId; public Detection(Integer batonId, Integer stationId, Integer rssi, Float battery, Long uptimeMs, Integer remoteId, Timestamp timestamp, Timestamp timestampIngestion) { this.batonId = batonId; @@ -27,75 +32,16 @@ public Detection(Integer batonId, Integer stationId, Integer rssi, Float battery this.timestampIngestion = timestampIngestion; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { + public Detection(Integer id, Integer stationId, Integer rssi) { this.id = id; - } - - public Integer getBatonId() { - return batonId; - } - - public void setBatonId(Integer batonId) { - this.batonId = batonId; - } - - public Integer getStationId() { - return stationId; - } - - public void setStationId(Integer stationId) { this.stationId = stationId; - } - - public Integer getRssi() { - return rssi; - } - - public void setRssi(Integer rssi) { this.rssi = rssi; } - public Float getBattery() { - return battery; - } - - public void setBattery(Float battery) { - this.battery = battery; - } - - public Long getUptimeMs() { - return uptimeMs; - } - - public void setUptimeMs(Long uptimeMs) { - this.uptimeMs = uptimeMs; - } - - public Integer getRemoteId() { - return remoteId; - } - - public void setRemoteId(Integer remoteId) { - this.remoteId = remoteId; - } - - public Timestamp getTimestamp() { - return timestamp; - } - - public void setTimestamp(Timestamp timestamp) { + public Detection(Integer id, Integer stationId, Integer rssi, Timestamp timestamp) { + this.id = id; + this.stationId = stationId; + this.rssi = rssi; this.timestamp = timestamp; } - - public Timestamp getTimestampIngestion() { - return timestampIngestion; - } - - public void setTimestampIngestion(Timestamp timestampIngestion) { - this.timestampIngestion = timestampIngestion; - } } diff --git a/src/main/java/telraam/database/models/Id.java b/src/main/java/telraam/database/models/Id.java deleted file mode 100644 index 97132af..0000000 --- a/src/main/java/telraam/database/models/Id.java +++ /dev/null @@ -1,19 +0,0 @@ -package telraam.database.models; - -public class Id { - - private int id; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - @Override - public String toString() { - return String.format("Id: %d", id); - } -} \ No newline at end of file diff --git a/src/main/java/telraam/database/models/Lap.java b/src/main/java/telraam/database/models/Lap.java index cf45139..7573562 100644 --- a/src/main/java/telraam/database/models/Lap.java +++ b/src/main/java/telraam/database/models/Lap.java @@ -1,52 +1,23 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.sql.Timestamp; +@Getter @Setter @NoArgsConstructor public class Lap { private Integer id; private Integer teamId; private Integer lapSourceId; + private Boolean manual; private Timestamp timestamp; - public Lap() { - } - public Lap(Integer teamId, Integer lapSourceId, Timestamp timestamp) { this.teamId = teamId; this.lapSourceId = lapSourceId; this.timestamp = timestamp; } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Integer getTeamId() { - return teamId; - } - - public void setTeamId(Integer teamId) { - this.teamId = teamId; - } - - public Integer getLapSourceId() { - return lapSourceId; - } - - public void setLapSourceId(Integer lapSourceId) { - this.lapSourceId = lapSourceId; - } - - public Timestamp getTimestamp() { - return timestamp; - } - - public void setTimestamp(Timestamp timestamp) { - this.timestamp = timestamp; - } } diff --git a/src/main/java/telraam/database/models/LapCount.java b/src/main/java/telraam/database/models/LapCount.java new file mode 100644 index 0000000..a3831a5 --- /dev/null +++ b/src/main/java/telraam/database/models/LapCount.java @@ -0,0 +1,11 @@ +package telraam.database.models; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter @Setter @NoArgsConstructor +public class LapCount { + private int teamId; + private int count; +} diff --git a/src/main/java/telraam/database/models/LapSource.java b/src/main/java/telraam/database/models/LapSource.java index 82ff02f..aa39fa3 100644 --- a/src/main/java/telraam/database/models/LapSource.java +++ b/src/main/java/telraam/database/models/LapSource.java @@ -1,33 +1,18 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + /** * The lap source tells you where the lap comes from. */ +@Getter @Setter @NoArgsConstructor public class LapSource { private Integer id; private String name; - public LapSource() { - - } - public LapSource(String name) { this.name = name; } - - public Integer getId() { - return id; - } - - public String getName() { - return name; - } - - public void setId(Integer id) { - this.id = id; - } - - public void setName(String name) { - this.name = name; - } } diff --git a/src/main/java/telraam/database/models/LapSourceSwitchover.java b/src/main/java/telraam/database/models/LapSourceSwitchover.java index 6d645ca..4b3a445 100644 --- a/src/main/java/telraam/database/models/LapSourceSwitchover.java +++ b/src/main/java/telraam/database/models/LapSourceSwitchover.java @@ -1,45 +1,23 @@ package telraam.database.models; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.sql.Timestamp; import java.util.Objects; +@Getter @Setter @NoArgsConstructor public class LapSourceSwitchover { private Integer id; private Integer newLapSource; private Timestamp timestamp; - // DO NOT REMOVE - public LapSourceSwitchover() {} - public LapSourceSwitchover(Integer newLapSource, Timestamp timestamp) { this.newLapSource = newLapSource; this.timestamp = timestamp; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Integer getNewLapSource() { - return newLapSource; - } - - public void setNewLapSource(Integer newLapSource) { - this.newLapSource = newLapSource; - } - - public Timestamp getTimestamp() { - return timestamp; - } - - public void setTimestamp(Timestamp timestamp) { - this.timestamp = timestamp; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/telraam/database/models/PositionSource.java b/src/main/java/telraam/database/models/PositionSource.java new file mode 100644 index 0000000..19d2fd1 --- /dev/null +++ b/src/main/java/telraam/database/models/PositionSource.java @@ -0,0 +1,17 @@ +package telraam.database.models; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class PositionSource { + private Integer id; + private String name; + + public PositionSource(String name) { + this.name = name; + } +} diff --git a/src/main/java/telraam/database/models/Station.java b/src/main/java/telraam/database/models/Station.java index 4439b83..b1f1220 100644 --- a/src/main/java/telraam/database/models/Station.java +++ b/src/main/java/telraam/database/models/Station.java @@ -1,83 +1,42 @@ package telraam.database.models; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter public class Station { private Integer id; private String name; private Double distanceFromStart; - private Boolean isBroken; + @Getter @Setter + private Boolean broken; private String url; private Double coordX; private Double coordY; public Station() { - this.isBroken = false; + this.broken = false; } public Station(String name, String url) { this.name = name; - this.isBroken = false; + this.broken = false; this.url = url; } public Station(String name, Double distanceFromStart, String url) { this.name = name; - this.isBroken = false; + this.broken = false; this.distanceFromStart = distanceFromStart; this.url = url; } public Station(String name, boolean isBroken) { this.name = name; - this.isBroken = isBroken; - } - - public Integer getId() { - return id; + this.broken = isBroken; } - public void setId(Integer id) { + public Station(Integer id) { this.id = id; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Double getDistanceFromStart() { - return distanceFromStart; - } - - public void setDistanceFromStart(Double distanceFromStart) { - this.distanceFromStart = distanceFromStart; - } - - public Boolean getIsBroken() { - return isBroken; - } - - public void setBroken(Boolean isBroken) { this.isBroken = isBroken; } - - public String getUrl() { - return this.url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Double getCoordX() { return this.coordX; }; - - public void setCoordX(Double coordX) { - this.coordX = coordX; - } - - public Double getCoordY() { return this.coordY; } - - public void setCoordY(Double coordY) { - this.coordY = coordY; - } } diff --git a/src/main/java/telraam/database/models/Team.java b/src/main/java/telraam/database/models/Team.java index 1713d93..317b2ed 100644 --- a/src/main/java/telraam/database/models/Team.java +++ b/src/main/java/telraam/database/models/Team.java @@ -1,14 +1,19 @@ package telraam.database.models; -import telraam.database.daos.BatonSwitchoverDAO; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import java.util.Objects; + +@Getter +@Setter +@NoArgsConstructor public class Team { private Integer id; private String name; private Integer batonId; - - public Team() { - } + private String jacketNr = "INVALID"; public Team(String name) { this.name = name; @@ -19,27 +24,7 @@ public Team(String name, int batonId) { this.batonId = batonId; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getBatonId() { - return batonId; - } - - public void setBatonId(Integer batonId) { - this.batonId = batonId; + public boolean equals(Team obj) { + return Objects.equals(id, obj.getId()); } } diff --git a/src/main/java/telraam/database/models/TeamLapCount.java b/src/main/java/telraam/database/models/TeamLapCount.java new file mode 100644 index 0000000..797c659 --- /dev/null +++ b/src/main/java/telraam/database/models/TeamLapCount.java @@ -0,0 +1,12 @@ +package telraam.database.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class TeamLapCount { + private Integer lapSourceId; + private Integer lapCount; +} diff --git a/src/main/java/telraam/logic/external/ExternalLapper.java b/src/main/java/telraam/logic/external/ExternalLapper.java deleted file mode 100644 index f82d55d..0000000 --- a/src/main/java/telraam/logic/external/ExternalLapper.java +++ /dev/null @@ -1,57 +0,0 @@ -package telraam.logic.external; - -import io.dropwizard.jersey.setup.JerseyEnvironment; -import org.jdbi.v3.core.Jdbi; -import telraam.database.daos.LapDAO; -import telraam.database.daos.LapSourceDAO; -import telraam.database.models.Detection; -import telraam.database.models.Lap; -import telraam.database.models.LapSource; -import telraam.logic.Lapper; - -import java.sql.Timestamp; -import java.util.LinkedList; -import java.util.List; - -public class ExternalLapper implements Lapper { - - static final String SOURCE_NAME = "external-lapper"; - private final LapDAO lapDAO; - private int lapSourceId; - - public ExternalLapper(Jdbi jdbi) { - this.lapDAO = jdbi.onDemand(LapDAO.class); - - // Get the lapSourceId, create the source if needed - LapSourceDAO lapSourceDAO = jdbi.onDemand(LapSourceDAO.class); - lapSourceDAO.getByName(SOURCE_NAME).ifPresentOrElse( - lapSource -> this.lapSourceId = lapSource.getId(), - () -> this.lapSourceId = lapSourceDAO.insert(new LapSource(SOURCE_NAME)) - ); - } - - @Override - public void handle(Detection msg) { - // Do nothing here. The external lappers polls periodically using the general api. - } - - public void saveLaps(List teamLaps) { - //TODO: Be less destructive on the database: Only delete and add the required laps. - lapDAO.deleteByLapSourceId(this.lapSourceId); - - LinkedList laps = new LinkedList<>(); - - for (ExternalLapperTeamLaps teamLap : teamLaps) { - for (ExternalLapperLap lap : teamLap.laps) { - laps.add(new Lap(teamLap.teamId, this.lapSourceId, new Timestamp((long) (lap.timestamp)))); - } - } - - lapDAO.insertAll(laps.iterator()); - } - - @Override - public void registerAPI(JerseyEnvironment jersey) { - jersey.register(new ExternalLapperResource(this)); - } -} diff --git a/src/main/java/telraam/logic/external/ExternalLapperLap.java b/src/main/java/telraam/logic/external/ExternalLapperLap.java deleted file mode 100644 index d694994..0000000 --- a/src/main/java/telraam/logic/external/ExternalLapperLap.java +++ /dev/null @@ -1,5 +0,0 @@ -package telraam.logic.external; - -public class ExternalLapperLap { - public double timestamp; -} diff --git a/src/main/java/telraam/logic/external/ExternalLapperResource.java b/src/main/java/telraam/logic/external/ExternalLapperResource.java deleted file mode 100644 index add3ceb..0000000 --- a/src/main/java/telraam/logic/external/ExternalLapperResource.java +++ /dev/null @@ -1,45 +0,0 @@ -package telraam.logic.external; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; - -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; - - -@Path("/lappers/external") -@Api("/lappers/external") -@Produces(MediaType.APPLICATION_JSON) -public class ExternalLapperResource { - private final ExternalLapper lapper; - - public ExternalLapperResource(ExternalLapper lapper) { - this.lapper = lapper; - } - - @POST - @Path("/laps") - @ApiOperation(value = "Post the current laps") - public void postLaps(List teamLaps) { - this.lapper.saveLaps(teamLaps); - } - - //TODO: Give the lapper an option to publish some of its internal state for debugging. - //@GET - //@Path("/stats") - //@ApiOperation(value = "Get lapper statistics") - //public Map> getStats() { - // //return this.lapper.getLapCounts(); - //} -// - //@POST - //@Path("/stats") - //@ApiOperation(value = "Post lapper statistics") - //public Map> postStats() { - // return this.lapper.getLapCounts(); - //} -} - diff --git a/src/main/java/telraam/logic/external/ExternalLapperStats.java b/src/main/java/telraam/logic/external/ExternalLapperStats.java deleted file mode 100644 index c285729..0000000 --- a/src/main/java/telraam/logic/external/ExternalLapperStats.java +++ /dev/null @@ -1,4 +0,0 @@ -package telraam.logic.external; - -public class ExternalLapperStats { -} diff --git a/src/main/java/telraam/logic/external/ExternalLapperTeamLaps.java b/src/main/java/telraam/logic/external/ExternalLapperTeamLaps.java deleted file mode 100644 index a003f3f..0000000 --- a/src/main/java/telraam/logic/external/ExternalLapperTeamLaps.java +++ /dev/null @@ -1,8 +0,0 @@ -package telraam.logic.external; - -import java.util.List; - -public class ExternalLapperTeamLaps { - public int teamId; - public List laps; -} diff --git a/src/main/java/telraam/logic/Lapper.java b/src/main/java/telraam/logic/lapper/Lapper.java similarity index 86% rename from src/main/java/telraam/logic/Lapper.java rename to src/main/java/telraam/logic/lapper/Lapper.java index 995d067..d999307 100644 --- a/src/main/java/telraam/logic/Lapper.java +++ b/src/main/java/telraam/logic/lapper/Lapper.java @@ -1,9 +1,10 @@ -package telraam.logic; +package telraam.logic.lapper; import io.dropwizard.jersey.setup.JerseyEnvironment; import telraam.database.models.Detection; public interface Lapper { void handle(Detection msg); + void registerAPI(JerseyEnvironment jersey); } diff --git a/src/main/java/telraam/logic/lapper/external/ExternalLapper.java b/src/main/java/telraam/logic/lapper/external/ExternalLapper.java new file mode 100644 index 0000000..510e28a --- /dev/null +++ b/src/main/java/telraam/logic/lapper/external/ExternalLapper.java @@ -0,0 +1,87 @@ +package telraam.logic.lapper.external; + +import io.dropwizard.jersey.setup.JerseyEnvironment; +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.LapDAO; +import telraam.database.daos.LapSourceDAO; +import telraam.database.models.Detection; +import telraam.database.models.Lap; +import telraam.database.models.LapSource; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.external.models.ExternalLapperTeamLaps; + +import java.sql.Timestamp; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class ExternalLapper implements Lapper { + + static final String SOURCE_NAME = "external-lapper"; + private final LapDAO lapDAO; + private int lapSourceId; + + public ExternalLapper(Jdbi jdbi) { + this.lapDAO = jdbi.onDemand(LapDAO.class); + + // Get the lapSourceId, create the source if needed + LapSourceDAO lapSourceDAO = jdbi.onDemand(LapSourceDAO.class); + lapSourceDAO.getByName(SOURCE_NAME).ifPresentOrElse( + lapSource -> this.lapSourceId = lapSource.getId(), + () -> this.lapSourceId = lapSourceDAO.insert(new LapSource(SOURCE_NAME)) + ); + } + + @Override + public void handle(Detection msg) { + // Do nothing here. The external lappers polls periodically using the general api. + } + + public void saveLaps(List teamLaps) { + List laps = lapDAO.getAllBySource(lapSourceId).stream().filter(l -> !l.getManual()).toList(); + + // Remember laps we have to take actions on + List lapsToDelete = new LinkedList<>(); + List lapsToAdd = new LinkedList<>(); + + // Find which laps are no longer needed or have to be added + for (ExternalLapperTeamLaps teamLap : teamLaps) { + List lapsForTeam = laps.stream().filter(l -> l.getTeamId() == teamLap.getTeamId()).sorted(Comparator.comparing(Lap::getTimestamp)).toList(); + List newLapsForTeam = teamLap.getLaps().stream().map(nl -> new Lap(teamLap.getTeamId(), lapSourceId, new Timestamp((long) (nl.getTimestamp())))).sorted(Comparator.comparing(Lap::getTimestamp)).toList(); + + int lapsIndex = 0; + int newLapsIndex = 0; + while (lapsIndex != lapsForTeam.size() || newLapsIndex != newLapsForTeam.size()) { + if (lapsIndex != lapsForTeam.size() && newLapsIndex != newLapsForTeam.size()) { + Lap lap = lapsForTeam.get(lapsIndex); + Lap newLap = newLapsForTeam.get(newLapsIndex); + if (lap.getTimestamp().before(newLap.getTimestamp())) { + lapsToDelete.add(lap); + lapsIndex++; + } else if (lap.getTimestamp().after(newLap.getTimestamp())) { + lapsToAdd.add(newLap); + newLapsIndex++; + } else { // Lap is present in both lists. Keep it. + lapsIndex++; + newLapsIndex++; + } + } else if (lapsIndex != lapsForTeam.size()) { + lapsToDelete.add(lapsForTeam.get(lapsIndex)); + lapsIndex++; + } else { + lapsToAdd.add(newLapsForTeam.get(newLapsIndex)); + newLapsIndex++; + } + } + } + + // Do the required actions + lapDAO.deleteAllById(lapsToDelete.iterator()); + lapDAO.insertAll(lapsToAdd.iterator()); + } + + @Override + public void registerAPI(JerseyEnvironment jersey) { + jersey.register(new ExternalLapperResource(this)); + } +} diff --git a/src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java b/src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java new file mode 100644 index 0000000..55f6aa0 --- /dev/null +++ b/src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java @@ -0,0 +1,45 @@ +package telraam.logic.lapper.external; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import telraam.logic.lapper.external.models.ExternalLapperStats; +import telraam.logic.lapper.external.models.ExternalLapperTeamLaps; + +import java.util.List; + + +@Path("/lappers/external") +@Produces(MediaType.APPLICATION_JSON) +public class ExternalLapperResource { + private final ExternalLapper lapper; + + private ExternalLapperStats externalLapperStats; + + public ExternalLapperResource(ExternalLapper lapper) { + this.lapper = lapper; + this.externalLapperStats = new ExternalLapperStats(); + } + + @POST + @Path("/laps") + @Operation(summary = "Post the current laps") + public void postLaps(List teamLaps) { + this.lapper.saveLaps(teamLaps); + } + + @GET + @Path("/stats") + @Operation(summary = "Get lapper statistics") + public ExternalLapperStats getStats() { + return externalLapperStats; + } + + @POST + @Path("/stats") + @Operation(summary = "Post lapper statistics") + public void postStats(ExternalLapperStats externalLapperStats) { + this.externalLapperStats = externalLapperStats; + } +} + diff --git a/src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java new file mode 100644 index 0000000..3623891 --- /dev/null +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java @@ -0,0 +1,9 @@ +package telraam.logic.lapper.external.models; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class ExternalLapperLap { + private double timestamp; +} diff --git a/src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java new file mode 100644 index 0000000..8c875bc --- /dev/null +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java @@ -0,0 +1,13 @@ +package telraam.logic.lapper.external.models; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter @Getter +public class ExternalLapperStats { + private List errorHistory; + private List> transitionMatrix; + private List> emissionMatrix; +} diff --git a/src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java new file mode 100644 index 0000000..fa37f63 --- /dev/null +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java @@ -0,0 +1,13 @@ +package telraam.logic.lapper.external.models; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + + +@Getter @Setter +public class ExternalLapperTeamLaps { + private int teamId; + private List laps; +} diff --git a/src/main/java/telraam/logic/robustLapper/RobustLapper.java b/src/main/java/telraam/logic/lapper/robust/RobustLapper.java similarity index 56% rename from src/main/java/telraam/logic/robustLapper/RobustLapper.java rename to src/main/java/telraam/logic/lapper/robust/RobustLapper.java index 6774299..62ae807 100644 --- a/src/main/java/telraam/logic/robustLapper/RobustLapper.java +++ b/src/main/java/telraam/logic/lapper/robust/RobustLapper.java @@ -1,11 +1,16 @@ -package telraam.logic.robustLapper; +package telraam.logic.lapper.robust; import io.dropwizard.jersey.setup.JerseyEnvironment; import org.jdbi.v3.core.Jdbi; -import telraam.database.daos.*; -import telraam.database.models.*; -import telraam.logic.Lapper; -import telraam.logic.viterbi.ViterbiLapper; +import telraam.database.daos.DetectionDAO; +import telraam.database.daos.LapDAO; +import telraam.database.daos.LapSourceDAO; +import telraam.database.daos.StationDAO; +import telraam.database.models.Detection; +import telraam.database.models.Lap; +import telraam.database.models.LapSource; +import telraam.database.models.Station; +import telraam.logic.lapper.Lapper; import java.sql.Timestamp; import java.util.*; @@ -13,7 +18,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import java.util.stream.Collectors; // Implement Lapper for easier use in App and Fetcher public class RobustLapper implements Lapper { @@ -30,14 +34,13 @@ public class RobustLapper implements Lapper { private boolean debounceScheduled; private int lapSourceId; private Map> teamDetections; - private List teams; private List stations; - private Map> teamLaps; + private Map> teamLaps; public RobustLapper(Jdbi jdbi) { this.jdbi = jdbi; this.scheduler = Executors.newScheduledThreadPool(1); - this.logger = Logger.getLogger(ViterbiLapper.class.getName()); + this.logger = Logger.getLogger(RobustLapper.class.getName()); this.lapDAO = jdbi.onDemand(LapDAO.class); this.debounceScheduled = false; @@ -49,47 +52,32 @@ public RobustLapper(Jdbi jdbi) { ); } + // Group all detections by time and keep only one detection per timestamp private void processData() { - // Maps a baton id to the current team using it - Map batonTeam = new HashMap<>(); - // Map containing all detections belonging to a team - teamDetections = new HashMap<>(); - - TeamDAO teamDAO = this.jdbi.onDemand(TeamDAO.class); DetectionDAO detectionDAO = this.jdbi.onDemand(DetectionDAO.class); - BatonSwitchoverDAO batonSwitchoverDAO = this.jdbi.onDemand(BatonSwitchoverDAO.class); - - teams = teamDAO.getAll(); - List detections = detectionDAO.getAll(); - List switchovers = batonSwitchoverDAO.getAll(); - - switchovers.sort(Comparator.comparing(BatonSwitchover::getTimestamp)); + List detections = detectionDAO.getAllWithTeamId(MIN_RSSI); detections.sort(Comparator.comparing(Detection::getTimestamp)); + teamDetections = new HashMap<>(); - Map teamById = teams.stream().collect(Collectors.toMap(Team::getId, team -> team)); - teams.forEach(team -> teamDetections.put(team.getId(), new ArrayList<>())); - - int switchoverIndex = 0; for (Detection detection : detections) { - // Switch teams batons if it happened before the current detection - while (switchoverIndex < switchovers.size() && switchovers.get(switchoverIndex).getTimestamp().before(detection.getTimestamp())) { - BatonSwitchover switchover = switchovers.get(switchoverIndex); - batonTeam.put(switchover.getNewBatonId(), teamById.get(switchover.getTeamId())); - batonTeam.remove(switchover.getPreviousBatonId()); - switchoverIndex++; - } - - // Check if detection belongs to a team, and it's signal is strong enough - if (batonTeam.containsKey(detection.getBatonId()) && detection.getRssi() > MIN_RSSI) { - List currentDetections = teamDetections.get(batonTeam.get(detection.getBatonId()).getId()); - // If team already has a detection for that timestamp keep the one with the strongest signal - if (! currentDetections.isEmpty() && currentDetections.get(currentDetections.size() - 1).getTimestamp().compareTo(detection.getTimestamp()) == 0) { - if (currentDetections.get(currentDetections.size() - 1).getRssi() < detection.getRssi()) { - currentDetections.remove(currentDetections.size() - 1); - currentDetections.add(detection); + if (detection.getTeamId() != null) { + if (teamDetections.containsKey(detection.getTeamId())) { + // teamDetections already contains teamId + List teamDetectionsList = teamDetections.get(detection.getTeamId()); + if (teamDetectionsList.get(teamDetectionsList.size() - 1).getTimestamp().compareTo(detection.getTimestamp()) == 0) { + // There's already a detection for that timestamp, keep the one with the highest rssi + if (teamDetectionsList.get(teamDetectionsList.size() - 1).getRssi() < detection.getRssi()) { + teamDetectionsList.remove(teamDetectionsList.size() - 1); + teamDetectionsList.add(detection); + } + } else { + // No detection yet for that timestamp so let's add it + teamDetectionsList.add(detection); } } else { - currentDetections.add(detection); + // Team id isn't in teamDetections yet so let's add it + teamDetections.put(detection.getTeamId(), new ArrayList<>()); + teamDetections.get(detection.getTeamId()).add(detection); } } } @@ -108,7 +96,7 @@ public void calculateLaps() { // List containing station id's and sorted based on their distance from the start List stationIdToPosition = stations.stream().map(Station::getId).toList(); - for (Team team : teams) { + for (Map.Entry> entry : teamDetections.entrySet()) { List lapTimes = new ArrayList<>(); // Station that is used for current interval @@ -117,9 +105,7 @@ public void calculateLaps() { int currentStationRssi = MIN_RSSI; int currentStationPosition = 0; - List detections = teamDetections.get(team.getId()); - - for (Detection detection : detections) { + for (Detection detection : entry.getValue()) { // Group all detections based on INTERVAL_TIME if (detection.getTimestamp().getTime() - currentStationTime < INTERVAL_TIME) { // We're still in the same interval @@ -132,7 +118,7 @@ public void calculateLaps() { } else { // We're in a new interval, use the detection with the highest RSSI to update trajectory // Check if new station is more likely to be in front of the runner - if (! (backwardPathDistance(lastStationPosition, currentStationPosition) <= 2)) { + if (!(backwardPathDistance(lastStationPosition, currentStationPosition) <= 3)) { if (isStartBetween(lastStationPosition, currentStationPosition)) { // Add lap if we passed the start line lapTimes.add(detection.getTimestamp()); @@ -146,7 +132,7 @@ public void calculateLaps() { } } // Save result for team - teamLaps.put(team.getId(), lapTimes); + teamLaps.put(entry.getKey(), lapTimes.stream().map(time -> new Lap(entry.getKey(), lapSourceId, time)).toList()); } save(); @@ -158,22 +144,60 @@ private int backwardPathDistance(int fromStation, int toStation) { return (((fromStation - toStation) % stations.size()) + stations.size()) % stations.size(); } - // Is the finish line between from_station and to_station when running forwards? + // Returns whether the finish line is between from_station and to_station when running forwards private boolean isStartBetween(int fromStation, int toStation) { return fromStation > toStation; } private void save() { - lapDAO.deleteByLapSourceId(this.lapSourceId); + // Get all the old laps and sort by team + List laps = lapDAO.getAllBySource(lapSourceId); + laps = laps.stream().filter(lap -> !lap.getManual()).toList(); + Map> oldLaps = new HashMap<>(); + + for (Integer teamId : teamLaps.keySet()) { + oldLaps.put(teamId, new ArrayList<>()); + } + + for (Lap lap : laps) { + oldLaps.get(lap.getTeamId()).add(lap); + } + + List lapsToUpdate = new ArrayList<>(); + List lapsToInsert = new ArrayList<>(); + List lapsToDelete = new ArrayList<>(); + + for (Map.Entry> entries : teamLaps.entrySet()) { + List newLapsTeam = entries.getValue(); + List oldLapsTeam = oldLaps.get(entries.getKey()); + oldLapsTeam.sort(Comparator.comparing(Lap::getTimestamp)); + int i = 0; + // Go over each lap and compare timestamp + while (i < oldLapsTeam.size() && i < newLapsTeam.size()) { + // Update the timestamp if it isn't equal + if (!oldLapsTeam.get(i).getTimestamp().equals(newLapsTeam.get(i).getTimestamp())) { + oldLapsTeam.get(i).setTimestamp(newLapsTeam.get(i).getTimestamp()); + lapsToUpdate.add(oldLapsTeam.get(i)); + } + i++; + } + + // More old laps so delete the surplus + while (i < oldLapsTeam.size()) { + lapsToDelete.add(oldLapsTeam.get(i)); + i++; + } - LinkedList laps = new LinkedList<>(); - for (Map.Entry> entries : teamLaps.entrySet()) { - for (Timestamp timestamp : entries.getValue()) { - laps.add(new Lap(entries.getKey(), lapSourceId, timestamp)); + // Add the new laps + while (i < newLapsTeam.size()) { + lapsToInsert.add(newLapsTeam.get(i)); + i++; } } - lapDAO.insertAll(laps.iterator()); + lapDAO.updateAll(lapsToUpdate.iterator()); + lapDAO.insertAll(lapsToInsert.iterator()); + lapDAO.deleteAll(lapsToDelete.iterator()); } @Override diff --git a/src/main/java/telraam/logic/SimpleLapper.java b/src/main/java/telraam/logic/lapper/simple/SimpleLapper.java similarity index 93% rename from src/main/java/telraam/logic/SimpleLapper.java rename to src/main/java/telraam/logic/lapper/simple/SimpleLapper.java index 5ed367e..608e724 100644 --- a/src/main/java/telraam/logic/SimpleLapper.java +++ b/src/main/java/telraam/logic/lapper/simple/SimpleLapper.java @@ -1,10 +1,11 @@ -package telraam.logic; +package telraam.logic.lapper.simple; import io.dropwizard.jersey.setup.JerseyEnvironment; import org.jdbi.v3.core.Jdbi; import telraam.database.daos.LapDAO; import telraam.database.daos.LapSourceDAO; import telraam.database.models.*; +import telraam.logic.lapper.Lapper; import java.util.ArrayList; import java.util.HashMap; @@ -13,7 +14,7 @@ public class SimpleLapper implements Lapper { // Needs to be the same as in the lap_source database table. - static final String SOURCE_NAME = "simple-lapper"; + public static final String SOURCE_NAME = "simple-lapper"; private static final int MAX_SPEED = 50; private final LapSource source; @@ -66,7 +67,8 @@ public void handle(Detection msg) { } @Override - public void registerAPI(JerseyEnvironment jersey) {} + public void registerAPI(JerseyEnvironment jersey) { + } private void generateLap(List detections) { Detection first = detections.get(0); diff --git a/src/main/java/telraam/logic/lapper/slapper/Slapper.java b/src/main/java/telraam/logic/lapper/slapper/Slapper.java new file mode 100644 index 0000000..e4a34af --- /dev/null +++ b/src/main/java/telraam/logic/lapper/slapper/Slapper.java @@ -0,0 +1,139 @@ +package telraam.logic.lapper.slapper; + +import io.dropwizard.jersey.setup.JerseyEnvironment; +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.LapSourceDAO; +import telraam.database.models.Detection; +import telraam.database.models.LapSource; +import telraam.logic.lapper.Lapper; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +// Lapper that only uses a single sql query +public class Slapper implements Lapper { + private final String SOURCE_NAME = "slapper"; + private final int DEBOUNCE_TIMEOUT = 10; + private final ScheduledExecutorService scheduler; + private boolean debounceScheduled; + private final Logger logger; + private final Jdbi jdbi; + + public Slapper(Jdbi jdbi) { + this.jdbi = jdbi; + this.scheduler = Executors.newScheduledThreadPool(1); + this.logger = Logger.getLogger(Slapper.class.getName()); + this.debounceScheduled = false; + + // Get the lapSourceId, create the source if needed + LapSourceDAO lapSourceDAO = jdbi.onDemand(LapSourceDAO.class); + if (lapSourceDAO.getByName(SOURCE_NAME).isEmpty()) { + lapSourceDAO.insert(new LapSource(SOURCE_NAME)); + } + } + + @Override + public void handle(Detection msg) { + if (!this.debounceScheduled) { + this.debounceScheduled = true; + this.scheduler.schedule(() -> { + try { + this.calculateLaps(); + } catch (Exception e) { + logger.severe(e.getMessage()); + } + this.debounceScheduled = false; + }, DEBOUNCE_TIMEOUT, TimeUnit.SECONDS); + } + } + + private void calculateLaps() { + logger.info("Slapper: Calculating laps..."); + this.jdbi.useHandle(handle -> handle.execute( + """ + WITH switchovers AS ( + SELECT teamid AS team_id, + newbatonid, + timestamp AS start_timestamp, + COALESCE( + LEAD(timestamp) OVER (PARTITION BY teamid ORDER BY timestamp), + timestamp + INTERVAL '1 MONTH' + ) AS next_baton_switch + FROM batonswitchover + ), + team_detections AS ( + SELECT station_id, + MAX(rssi) as rssi, + date_trunc('second', timestamp) AS timestamp_seconds, + team_id + FROM detection d + LEFT JOIN switchovers s ON d.baton_id = s.newbatonid + AND d.timestamp BETWEEN s.start_timestamp AND s.next_baton_switch + WHERE station_id NOT BETWEEN 3 AND 5 + AND rssi > -84 + AND team_id IS NOT NULL + GROUP BY date_trunc('second', timestamp), team_id, station_id + ), + start_times AS ( + SELECT DISTINCT ON (team_id) team_id, + timestamp_seconds AS start_seconds + FROM team_detections + WHERE station_id BETWEEN 2 AND 3 + ORDER BY team_id, timestamp_seconds + ), + new_laps AS ( + SELECT previous.team_id, + timestamp_seconds + FROM ( + SELECT *, + LAG(station_id) OVER ( + PARTITION BY team_id + ORDER BY timestamp_seconds + ) AS prev_station_id + FROM team_detections + ) AS previous + LEFT JOIN start_times s_t + ON previous.team_id = s_t.team_id + WHERE station_id - prev_station_id < -4 + AND timestamp_seconds > start_seconds + ), + cst_source_id AS ( + SELECT COALESCE(id, -1) AS source_id + FROM lap_source + WHERE name ILIKE 'slapper' + ), + deletions AS ( + DELETE FROM lap l + WHERE lap_source_id = (SELECT source_id FROM cst_source_id) + AND NOT EXISTS ( + SELECT 1 + FROM new_laps n_l + WHERE l.team_id = n_l.team_id + AND l.timestamp = n_l.timestamp_seconds + ) + ) + INSERT INTO lap (team_id, timestamp, lap_source_id) + SELECT team_id, + timestamp_seconds, + source_id + FROM new_laps n_l, cst_source_id + WHERE NOT EXISTS ( + SELECT 1 + FROM lap l, cst_source_id + WHERE l.lap_source_id = source_id + AND l.team_id = n_l.team_id + AND l.timestamp = n_l.timestamp_seconds + ) + """ + ) + ); + logger.info("Slapper: Done calculating laps"); + } + + @Override + public void registerAPI(JerseyEnvironment jersey) { + + } +} diff --git a/src/main/java/telraam/logic/positioner/Position.java b/src/main/java/telraam/logic/positioner/Position.java new file mode 100644 index 0000000..28a4e37 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/Position.java @@ -0,0 +1,14 @@ +package telraam.logic.positioner; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.Setter; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record Position ( + int teamId, + double progress, + double speed, + long timestamp +) {} diff --git a/src/main/java/telraam/logic/positioner/PositionSender.java b/src/main/java/telraam/logic/positioner/PositionSender.java new file mode 100644 index 0000000..a111746 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/PositionSender.java @@ -0,0 +1,28 @@ +package telraam.logic.positioner; + +import telraam.websocket.WebSocketMessage; +import telraam.websocket.WebSocketMessageSingleton; + +import java.util.List; +import java.util.Map; + +public class PositionSender { + private final WebSocketMessage> message = new WebSocketMessage<>(); + private final String name; + + public PositionSender(String name) { + this.message.setTopic("position"); + this.name = name; + } + + public void send(List positions) { + Map payload = Map.of( + "positioner", this.name, + "positions", positions + ); + + message.setData(payload); + WebSocketMessageSingleton.getInstance().sendToAll(message); + } + +} diff --git a/src/main/java/telraam/logic/positioner/Positioner.java b/src/main/java/telraam/logic/positioner/Positioner.java new file mode 100644 index 0000000..4128b36 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/Positioner.java @@ -0,0 +1,8 @@ +package telraam.logic.positioner; + +import telraam.database.models.Detection; + +public interface Positioner { + void handle(Detection detection); + +} diff --git a/src/main/java/telraam/logic/positioner/Stationary/Stationary.java b/src/main/java/telraam/logic/positioner/Stationary/Stationary.java new file mode 100644 index 0000000..e34c8ef --- /dev/null +++ b/src/main/java/telraam/logic/positioner/Stationary/Stationary.java @@ -0,0 +1,54 @@ +package telraam.logic.positioner.Stationary; + +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.PositionSourceDAO; +import telraam.database.daos.TeamDAO; +import telraam.database.models.Detection; +import telraam.database.models.PositionSource; +import telraam.database.models.Team; +import telraam.logic.positioner.Position; +import telraam.logic.positioner.PositionSender; +import telraam.logic.positioner.Positioner; + +import java.util.List; +import java.util.logging.Logger; + +public class Stationary implements Positioner { + private static final Logger logger = Logger.getLogger(Stationary.class.getName()); + private final String SOURCE_NAME = "stationary"; + private final int INTERVAL_UPDATE_MS = 60000; + private final Jdbi jdbi; + private final PositionSender positionSender; + public Stationary(Jdbi jdbi) { + this.jdbi = jdbi; + this.positionSender = new PositionSender(SOURCE_NAME); + + // Add as source + PositionSourceDAO positionSourceDAO = jdbi.onDemand(PositionSourceDAO.class); + if (positionSourceDAO.getByName(SOURCE_NAME).isEmpty()) { + positionSourceDAO.insert(new PositionSource(SOURCE_NAME)); + } + + new Thread(this::update).start(); + } + + private void update() { + // Keep sending updates in case Loxsi ever restarts + while (true) { + long timestamp = System.currentTimeMillis(); + List teams = jdbi.onDemand(TeamDAO.class).getAll(); + + List positions = teams.stream().map(t -> new Position(t.getId(), 0, 0, timestamp)).toList(); + positionSender.send(positions); + + try { + Thread.sleep(INTERVAL_UPDATE_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + } + + @Override + public void handle(Detection detection) {} +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v1/CircularQueueV1.java b/src/main/java/telraam/logic/positioner/nostradamus/v1/CircularQueueV1.java new file mode 100644 index 0000000..95889cc --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v1/CircularQueueV1.java @@ -0,0 +1,22 @@ +package telraam.logic.positioner.nostradamus.v1; + +import java.util.LinkedList; + +// LinkedList with a maximum length +public class CircularQueueV1 extends LinkedList { + + private final int maxSize; + public CircularQueueV1(int maxSize) { + this.maxSize = maxSize; + } + + @Override + public boolean add(T e) { + if (size() >= this.maxSize) { + removeFirst(); + } + + return super.add(e); + } + +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v1/DetectionListV1.java b/src/main/java/telraam/logic/positioner/nostradamus/v1/DetectionListV1.java new file mode 100644 index 0000000..db7e0f6 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v1/DetectionListV1.java @@ -0,0 +1,61 @@ +package telraam.logic.positioner.nostradamus.v1; + +import lombok.Getter; +import telraam.database.models.Detection; +import telraam.database.models.Station; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class DetectionListV1 extends ArrayList { + + private final int interval; + private final List stations; + @Getter + private Detection currentPosition; + private Timestamp newestDetection; + + public DetectionListV1(int interval, List stations) { + this.interval = interval; + this.stations = stations.stream().sorted(Comparator.comparing(Station::getDistanceFromStart)).map(Station::getId).toList(); + this.currentPosition = new Detection(-1, 0, -100); + this.newestDetection = new Timestamp(0); + } + + // Returns True if the added detection results in a new station + @Override + public boolean add(Detection e) { + super.add(e); + + if (e.getTimestamp().after(newestDetection)) { + newestDetection = e.getTimestamp(); + } + + if (!e.getStationId().equals(currentPosition.getStationId()) && stationAfter(currentPosition.getStationId(), e.getStationId())) { + // Possible new position + if (e.getRssi() > currentPosition.getRssi() || !inInterval(currentPosition.getTimestamp(), newestDetection)) { + // Detection stored in currentPosition will change + int oldPosition = currentPosition.getStationId(); + // Filter out old detections + removeIf(detection -> !inInterval(detection.getTimestamp(), newestDetection)); + + // Get new position + currentPosition = stream().max(Comparator.comparing(Detection::getRssi)).get(); + + return oldPosition != currentPosition.getStationId(); + } + } + + return false; + } + + private boolean stationAfter(int oldStationId, int newStationId) { + return (stations.indexOf(newStationId) - stations.indexOf(oldStationId) + stations.size()) % stations.size() > 0; + } + + private boolean inInterval(Timestamp oldest, Timestamp newest) { + return newest.getTime() - oldest.getTime() < interval; + } +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v1/NostradamusV1.java b/src/main/java/telraam/logic/positioner/nostradamus/v1/NostradamusV1.java new file mode 100644 index 0000000..d8b875c --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v1/NostradamusV1.java @@ -0,0 +1,162 @@ +package telraam.logic.positioner.nostradamus.v1; + +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.BatonSwitchoverDAO; +import telraam.database.daos.PositionSourceDAO; +import telraam.database.daos.StationDAO; +import telraam.database.daos.TeamDAO; +import telraam.database.models.*; +import telraam.logic.positioner.Position; +import telraam.logic.positioner.PositionSender; +import telraam.logic.positioner.Positioner; + +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class NostradamusV1 implements Positioner { + private static final Logger logger = Logger.getLogger(NostradamusV1.class.getName()); + private final String SOURCE_NAME = "nostradamus_v1"; + private final int INTERVAL_CALCULATE_MS = 500; // How often to handle new detections (in milliseconds) + private final int INTERVAL_FETCH_MS = 10000; // Interval between fetching baton switchovers (in milliseconds) + private final int INTERVAL_DETECTIONS_MS = 3000; // Amount of milliseconds to group detections by + private final int MAX_NO_DATA_MS = 30000; // Send a stationary position after receiving no station update for x amount of milliseconds + private final int MEDIAN_AMOUNT = 10; // Calculate the median running speed of the last x intervals + private final double AVERAGE_SPRINTING_SPEED_M_MS = 0.00684; // Average sprinting speed meters / milliseconds + private final int MIN_RSSI = -84; // Minimum rssi strength for a detection + private final int FINISH_OFFSET_M = 0; // Distance between the last station and the finish in meters + private final Jdbi jdbi; + private final List newDetections; // Contains not yet handled detections + private Map batonToTeam; // Baton ID to Team ID + private final Map teamData; // All team data + private final PositionSender positionSender; + private final Lock detectionLock; + private final Lock dataLock; + + public NostradamusV1(Jdbi jdbi) { + this.jdbi = jdbi; + + PositionSourceDAO positionSourceDAO = jdbi.onDemand(PositionSourceDAO.class); + if (positionSourceDAO.getByName(SOURCE_NAME).isEmpty()) { + positionSourceDAO.insert(new PositionSource(SOURCE_NAME)); + } + + this.newDetections = new ArrayList<>(); + this.detectionLock = new ReentrantLock(); + this.dataLock = new ReentrantLock(); + + // Will be filled by fetch + this.batonToTeam = new HashMap<>(); + this.teamData = getTeamData(); + + this.positionSender = new PositionSender(SOURCE_NAME); + + new Thread(this::fetch).start(); + new Thread(this::calculatePosition).start(); + } + + // Initiate the team data map + private Map getTeamData() { + List stations = jdbi.onDemand(StationDAO.class).getAll(); + stations.sort(Comparator.comparing(Station::getDistanceFromStart)); + List teams = jdbi.onDemand(TeamDAO.class).getAll(); + + return teams.stream().collect(Collectors.toMap( + Team::getId, + team -> new TeamDataV1(team.getId(), INTERVAL_DETECTIONS_MS, stations, MEDIAN_AMOUNT, AVERAGE_SPRINTING_SPEED_M_MS, FINISH_OFFSET_M) + )); + } + + // Fetch all baton switchovers and replace the current one if there are any changes + private void fetch() { + List switchovers = jdbi.onDemand(BatonSwitchoverDAO.class).getAll(); + + Map batonToTeam = switchovers.stream().sorted( + Comparator.comparing(BatonSwitchover::getTimestamp) + ).collect(Collectors.toMap( + BatonSwitchover::getNewBatonId, + BatonSwitchover::getTeamId, + (existing, replacement) -> replacement + )); + + if (!this.batonToTeam.equals(batonToTeam)) { + dataLock.lock(); + this.batonToTeam = batonToTeam; + dataLock.unlock(); + } + + // Sleep tight + try { + Thread.sleep(INTERVAL_FETCH_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + + // handle all new detections and update positions accordingly + private void calculatePosition() { + Set changedTeams = new HashSet<>(); // List of teams that have changed station + while (true) { + changedTeams.clear(); + dataLock.lock(); + detectionLock.lock(); + for (Detection detection: newDetections) { + if (batonToTeam.containsKey(detection.getBatonId())) { + Integer teamId = batonToTeam.get(detection.getBatonId()); + if (teamData.get(teamId).addDetection(detection)) { + changedTeams.add(teamId); + } + } + } + newDetections.clear(); + detectionLock.unlock(); // Use lock as short as possible + dataLock.unlock(); + + if (!changedTeams.isEmpty()) { + // Update + for (Integer teamId: changedTeams) { + teamData.get(teamId).updatePosition(); + } + + // Send new data to the websocket + positionSender.send( + changedTeams.stream().map(team -> teamData.get(team).getPosition()).toList() + ); + } + + // Send a stationary position if no new station data was received recently + long now = System.currentTimeMillis(); + for (Map.Entry entry: teamData.entrySet()) { + if (now - entry.getValue().getPreviousStationArrival() > MAX_NO_DATA_MS) { + positionSender.send( + Collections.singletonList(new Position( + entry.getKey(), + entry.getValue().getPosition().progress(), + 0, + System.currentTimeMillis() + )) + ); + entry.getValue().setPreviousStationArrival(entry.getValue().getPreviousStationArrival() + MAX_NO_DATA_MS); + } + } + + // Goodnight + try { + Thread.sleep(INTERVAL_CALCULATE_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + } + + @Override + public void handle(Detection detection) { + if (detection.getRssi() > MIN_RSSI) { + detectionLock.lock(); + newDetections.add(detection); + detectionLock.unlock(); + } + } +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v1/StationDataV1.java b/src/main/java/telraam/logic/positioner/nostradamus/v1/StationDataV1.java new file mode 100644 index 0000000..15c7277 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v1/StationDataV1.java @@ -0,0 +1,38 @@ +package telraam.logic.positioner.nostradamus.v1; + +import telraam.database.models.Station; + +import java.util.ArrayList; +import java.util.List; + +// Record containing all data necessary for TeamData +public record StationDataV1( + Station station, // The station + Station nextStation, // The next station + List times, // List containing the times (in ms) that was needed to run from this station to the next one. + int index, // Index of this station when sorting a station list by distanceFromStart + float currentProgress, // The progress value of this station + float nextProgress // The progress value of the next station +) { + public StationDataV1() { + this( + new Station(-10), + new Station(-9), + new ArrayList<>(0), + -10, + 0F, + 0F + ); + } + + public StationDataV1(List stations, int index, int averageAmount, int totalDistance) { + this( + stations.get(index), + stations.get((index + 1) % stations.size()), + new CircularQueueV1<>(averageAmount), + index, + (float) (stations.get(index).getDistanceFromStart() / totalDistance), + (float) (stations.get((index + 1) % stations.size()).getDistanceFromStart() / totalDistance) + ); + } +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v1/TeamDataV1.java b/src/main/java/telraam/logic/positioner/nostradamus/v1/TeamDataV1.java new file mode 100644 index 0000000..82de61b --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v1/TeamDataV1.java @@ -0,0 +1,124 @@ +package telraam.logic.positioner.nostradamus.v1; + +import lombok.Getter; +import lombok.Setter; +import telraam.database.models.Detection; +import telraam.database.models.Station; +import telraam.logic.positioner.Position; + +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class TeamDataV1 { + private static final Logger logger = Logger.getLogger(TeamDataV1.class.getName()); + private final DetectionListV1 detections; // List with all relevant detections + private final Map stations; // Station list + private StationDataV1 currentStation; // Current station location + private StationDataV1 previousStation; // Previous station location + @Getter @Setter + private long previousStationArrival; // Arrival time of previous station. Used to calculate the average times + private final int totalDistance; // Total distance of the track + private final float maxDeviance; // Maximum deviance the animation can have from the reality + @Getter + private Position position; // Data to send to the websocket + private final int teamId; + + + public TeamDataV1(int teamId, int interval, List stations, int averageAmount, double sprintingSpeed, int finishOffset) { + stations.sort(Comparator.comparing(Station::getDistanceFromStart)); + this.totalDistance = (int) (stations.get(stations.size() - 1).getDistanceFromStart() + finishOffset); + this.stations = stations.stream().collect(Collectors.toMap( + Station::getId, + station -> new StationDataV1( + stations, + stations.indexOf(station), + averageAmount, + totalDistance + ) + )); + // Pre-populate with some default values + this.stations.forEach((stationId, stationData) -> stationData.times().add( + (long) (((stationData.nextStation().getDistanceFromStart() - stationData.station().getDistanceFromStart() + totalDistance) % totalDistance) / sprintingSpeed) + )); + this.detections = new DetectionListV1(interval, stations); + this.previousStationArrival = System.currentTimeMillis(); + this.currentStation = new StationDataV1(); // Will never trigger `isNextStation` for the first station + this.maxDeviance = (float) 1 / stations.size(); + this.teamId = teamId; + this.position = new Position(teamId, 0, 0, System.currentTimeMillis()); + } + + // Add a new detection + // Returns true if the team is at a new station + public boolean addDetection(Detection e) { + boolean newStation = detections.add(e); + + if ((newStation && isForwardStation(currentStation.index(), stations.get(e.getStationId()).index())) || currentStation.index() == -10) { + // Is at a new station that is in front of the previous one. + previousStation = currentStation; + currentStation = stations.get(e.getStationId()); + long now = System.currentTimeMillis(); + if (isNextStation()) { + // Only add the time if it goes forward by exactly one station + previousStation.times().add(now - previousStationArrival); + } + previousStationArrival = now; + + return true; + } + + return false; + } + + private boolean isForwardStation(int oldStation, int newStation) { + int stationDiff = (newStation - oldStation + stations.size()) % stations.size(); + return stationDiff < 3; + } + + private boolean isNextStation() { + return Objects.equals(previousStation.nextStation().getId(), currentStation.station().getId()); + } + + private double normalize(double number) { + return (number + 1) % 1; + } + + // Update the position data + public void updatePosition() { + long currentTime = System.currentTimeMillis(); + + // Animation is currently at progress x + long milliSecondsSince = currentTime - position.timestamp(); + double theoreticalProgress = normalize(position.progress() + (position.speed() * milliSecondsSince)); + + // Arrive at next station at timestamp y and progress z + double median = getMedian(); + double nextStationArrival = currentTime + median; + double goalProgress = currentStation.nextProgress(); + + double speed, progress; + // Determine whether to speed up / slow down the animation or teleport it + double difference = normalize(currentStation.currentProgress() - theoreticalProgress); + if ((difference >= maxDeviance && difference <= 1 - maxDeviance)) { + // Animation was too far behind or ahead so teleport + progress = currentStation.currentProgress(); + speed = normalize(currentStation.nextProgress() - progress) / median; + } else { + // Animation is close enough, adjust so that we're synced at the next station + progress = theoreticalProgress; + speed = normalize(goalProgress - theoreticalProgress) / (nextStationArrival - currentTime); + } + + position = new Position(teamId, progress, speed, currentTime); + } + + // Get the medium of the average times + private long getMedian() { + List sortedList = new ArrayList<>(currentStation.times()); + Collections.sort(sortedList); + + int size = sortedList.size(); + return size % 2 == 0 ? (sortedList.get(size / 2 - 1) + sortedList.get(size / 2)) / 2 : (sortedList.get(size / 2)); + } +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v2/Nostradamus.java b/src/main/java/telraam/logic/positioner/nostradamus/v2/Nostradamus.java new file mode 100644 index 0000000..05652ae --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v2/Nostradamus.java @@ -0,0 +1,150 @@ +package telraam.logic.positioner.nostradamus.v2; + +import org.jdbi.v3.core.Jdbi; +import telraam.AppConfiguration; +import telraam.database.daos.BatonSwitchoverDAO; +import telraam.database.daos.PositionSourceDAO; +import telraam.database.daos.StationDAO; +import telraam.database.models.*; +import telraam.logic.positioner.Position; +import telraam.logic.positioner.PositionSender; +import telraam.logic.positioner.Positioner; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class Nostradamus implements Positioner { + private static final Logger logger = Logger.getLogger(Nostradamus.class.getName()); + private final String SOURCE_NAME = "nostradamus"; + private final int MIN_RSSI = -84; // Minimum rssi strength that a detections needs to have + private final int INTERVAL_FETCH_MS = 10000; + private final int INTERVAL_UPDATE_MS = 200; + private final double MAX_SPEED_M_MS = 0.00972222; // Maximum speed (m / ms) = 35 km / h + private final Jdbi jdbi; + private final PositionSender positionSender; + ConcurrentHashMap> newDetections; + private final Map teamHandlers; + private final Map stationData; + private final double maxSpeedProgressMs; // Maximum speed (progress / ms) + + public Nostradamus(AppConfiguration configuration, Jdbi jdbi) { + this.jdbi = jdbi; + + // Add as source + PositionSourceDAO positionSourceDAO = jdbi.onDemand(PositionSourceDAO.class); + if (positionSourceDAO.getByName(SOURCE_NAME).isEmpty()) { + positionSourceDAO.insert(new PositionSource(SOURCE_NAME)); + } + + this.positionSender = new PositionSender(SOURCE_NAME); + this.newDetections = new ConcurrentHashMap<>(); + this.teamHandlers = new ConcurrentHashMap<>(); + this.stationData = new HashMap<>(); + + // Initialize station data list + List stations = this.jdbi.onDemand(StationDAO.class).getAll(); + stations.sort(Comparator.comparing(Station::getDistanceFromStart)); + int length = (int) (stations.get(stations.size() - 1).getDistanceFromStart() + configuration.getFinishOffset()); + for (int i = 0; i < stations.size(); i++) { + Station station = stations.get(i); + int nextIdx = (i + 1) % stations.size(); + int distanceToNext = (int) ((stations.get(nextIdx).getDistanceFromStart() - station.getDistanceFromStart() + length ) % length); + this.stationData.put(station.getId(), new StationData( + distanceToNext, + station.getDistanceFromStart() / length, + (double) distanceToNext / length, + stations.get(nextIdx).getId(), + i + )); + } + + this.maxSpeedProgressMs = MAX_SPEED_M_MS / (stations.get(stations.size() - 1).getDistanceFromStart() + configuration.getFinishOffset()); + + new Thread(this::fetch).start(); + new Thread(this::update).start(); + } + + // Fetch updates team handlers based on switchovers + private void fetch() { + while (true) { + List switchovers = jdbi.onDemand(BatonSwitchoverDAO.class).getAll(); + + Map batonToTeam = switchovers.stream() + .filter(switchover -> switchover.getNewBatonId() != null) + .sorted(Comparator.comparing(BatonSwitchover::getTimestamp)) + .collect(Collectors.toMap( + BatonSwitchover::getNewBatonId, + BatonSwitchover::getTeamId, + (existing, replacement) -> replacement + )); + + for (Map.Entry entry: batonToTeam.entrySet()) { + teamHandlers.compute(entry.getValue(), (teamId, existingTeam) -> { + if (existingTeam == null) { + return new TeamHandler(teamId, new AtomicInteger(entry.getKey()), maxSpeedProgressMs, stationData); + } else { + existingTeam.batonId.set(entry.getKey()); + return existingTeam; + } + }); + } + + try { + Thread.sleep(INTERVAL_FETCH_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + } + + // Update handles all new detections and sends new positions + private void update() { + List positions = new ArrayList<>(); + + while (true) { + positions.clear(); + + for (TeamHandler team : teamHandlers.values()) { + ConcurrentLinkedQueue queue = newDetections.get(team.batonId.get()); + if (queue != null && !queue.isEmpty()) { + List copy = new ArrayList<>(); + Detection d; + while ((d = queue.poll()) != null) { + copy.add(d); + } + + team.update(copy); + } + + Position position = team.getPosition(); + if (position != null) { + positions.add(position); + } + } + + if (!positions.isEmpty()) { + positionSender.send(positions); + } + + try { + Thread.sleep(INTERVAL_UPDATE_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + } + + @Override + public void handle(Detection detection) { + if (detection.getRssi() > MIN_RSSI) { + newDetections + .computeIfAbsent(detection.getBatonId(), k -> new ConcurrentLinkedQueue<>()) + .add(detection); + } + } + +} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v2/StationData.java b/src/main/java/telraam/logic/positioner/nostradamus/v2/StationData.java new file mode 100644 index 0000000..9f2b95d --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v2/StationData.java @@ -0,0 +1,10 @@ +package telraam.logic.positioner.nostradamus.v2; + +// Record containing all data regarding a station +public record StationData( + int distanceToNext, // Meters until the next station + double progress, // Location of station in progress + double progressToNext, // Progress until you arrive at the next station + int nextStationId, // ID of the next station + int index // Index of the station when sorted by distance from the start +) {} diff --git a/src/main/java/telraam/logic/positioner/nostradamus/v2/TeamHandler.java b/src/main/java/telraam/logic/positioner/nostradamus/v2/TeamHandler.java new file mode 100644 index 0000000..c7e31de --- /dev/null +++ b/src/main/java/telraam/logic/positioner/nostradamus/v2/TeamHandler.java @@ -0,0 +1,169 @@ +package telraam.logic.positioner.nostradamus.v2; + +import telraam.database.models.Detection; +import telraam.logic.positioner.Position; + +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +public class TeamHandler { + private static final Logger logger = Logger.getLogger(TeamHandler.class.getName()); + private final double AVG_SPEED = 0.006; // Average sprinting speed (m / ms), results in a lap of 55 seconds in the 12ul + private final int INTERVAL = 2000; // Only keep detections in a x ms interval + private final int MAX_TIMES = 20; // Amount of speeds to keep track of to determine the median + private final int teamId; + public AtomicInteger batonId; + private final double maxSpeed; + private final Map stationDataMap; // Map from station id to StationData + private final Map> stationSpeeds; // Avg speed (progress / ms) to go from a stationId to the next + private int currentStation; // Current station id + private Position lastPosition; + private final Queue positions; + private final LinkedList detections; + private Detection currentStationDetection; + + public TeamHandler(int teamId, AtomicInteger batonId, double maxSpeed, Map stationDataMap) { + this.teamId = teamId; + this.batonId = batonId; + this.maxSpeed = maxSpeed; + this.stationDataMap = stationDataMap; + + this.stationSpeeds = new HashMap<>(); + this.positions = new ArrayDeque<>(); + this.detections = new LinkedList<>(); + + this.currentStation = -1; + this.lastPosition = new Position(0, 0, 0, 0); + this.detections.add(new Detection(-1, -1, -1000, new Timestamp(0))); + + // Populate the stationSpeeds map with default values + for (Map.Entry entry: stationDataMap.entrySet()) { + this.stationSpeeds.put(entry.getKey(), new ArrayList<>()); + double progress = stationDataMap.get(entry.getKey()).progressToNext(); + double time = entry.getValue().distanceToNext() / AVG_SPEED; + this.stationSpeeds.get(entry.getKey()).add(progress / time); + } + } + + public void update(List detections) { + boolean newStation = handleDetection(detections); + if (!newStation) { + return; + } + + StationData station = stationDataMap.get(currentStation); + long timestamp = System.currentTimeMillis(); + + double currentProgress = normalize(lastPosition.progress() + lastPosition.speed() * (timestamp - lastPosition.timestamp())); // Where is the animation now + + double maxDeviation = station.progressToNext(); + if (circularDistance(currentProgress, station.progress()) > maxDeviation) { + // Don't let the animation deviate too much from the reality + currentProgress = station.progress(); + } + + long intervalTime = (long) (station.progressToNext() / getMedianSpeed(currentStation)); // How many ms until it should reach the next station + double goalProgress = normalize(station.progress() + station.progressToNext()); // Where is the next station + double speed = normalize(goalProgress - currentProgress) / intervalTime; + + if (speed > maxSpeed) { + // Sanity check + currentProgress = stationDataMap.get(currentStation).progress(); + speed = getMedianSpeed(currentStation); + } + + positions.clear(); + positions.add(new Position(teamId, currentProgress, speed, timestamp)); + } + + public Position getPosition() { + if (!positions.isEmpty()) { + lastPosition = positions.poll(); + return lastPosition; + } + + return null; + } + + private boolean handleDetection(List newDetections) { + boolean newStation = false; + + newDetections.sort(Comparator.comparing(Detection::getTimestamp)); + for (Detection detection: newDetections) { + if (!detection.getTimestamp().after(detections.getLast().getTimestamp())) { + // Only keep newer detections + continue; + } + + detections.add(detection); // Newest detection is now at the end of the list + + if (detection.getStationId() == currentStation) { + // We've already determined that we have arrived at this station + continue; + } + + // Filter out old detections + long lastDetection = detections.stream().max(Comparator.comparing(d -> d.getTimestamp().getTime())).get().getTimestamp().getTime(); + detections.removeIf(d -> lastDetection - d.getTimestamp().getTime() > INTERVAL); + + // Determine new position + int newStationId = detections.stream().max(Comparator.comparing(Detection::getRssi)).get().getStationId(); // detections will at least contain the last detection + if (currentStation != newStationId && stationAfter(newStationId)) { + // New position! + // Add new speed + if (currentStationDetection != null && newStationId == stationDataMap.get(currentStation).nextStationId()) { // Necessary for the first station switch + double progress = normalize(stationDataMap.get(newStationId).progress() - stationDataMap.get(currentStation).progress()); + double time = detection.getTimestamp().getTime() - currentStationDetection.getTimestamp().getTime(); + stationSpeeds.get(currentStation).add(progress / time); + } + + // Update station variables + currentStation = newStationId; + currentStationDetection = detection; + newStation = true; + } + } + + return newStation; + } + + private boolean stationAfter(int newStationId) { + if (currentStationDetection == null) { + return true; + } + + int stations = stationDataMap.size(); + return (((stationDataMap.get(newStationId).index() - stationDataMap.get(currentStation).index()) % stations) + stations) % stations < 4; + } + + private double getMedianSpeed(int stationId) { + List times = stationSpeeds.get(stationId); + if (times.size() > MAX_TIMES) { + times.subList(0, times.size() - MAX_TIMES).clear(); + } + + List copy = new ArrayList<>(times); + Collections.sort(copy); + + double median; + if (copy.size() % 2 == 0) { + median = (copy.get(copy.size() / 2) + copy.get(copy.size() / 2 - 1)) / 2; + } else { + median = copy.get(copy.size() / 2); + } + + return median; + } + + private double circularDistance(double a, double b) { + double diff = Math.abs(a - b); + return Math.min(diff, 1 - diff); + } + + private double normalize(double amount) { + return ((amount % 1) + 1) % 1; + } + +} diff --git a/src/main/java/telraam/logic/viterbi/ViterbiLapper.java b/src/main/java/telraam/logic/viterbi/ViterbiLapper.java deleted file mode 100644 index 980d891..0000000 --- a/src/main/java/telraam/logic/viterbi/ViterbiLapper.java +++ /dev/null @@ -1,292 +0,0 @@ -package telraam.logic.viterbi; - -import io.dropwizard.jersey.setup.JerseyEnvironment; -import org.jdbi.v3.core.Jdbi; -import telraam.database.daos.*; -import telraam.database.models.*; -import telraam.logic.Lapper; -import telraam.logic.viterbi.algorithm.ViterbiAlgorithm; -import telraam.logic.viterbi.algorithm.ViterbiModel; -import telraam.logic.viterbi.algorithm.ViterbiState; - -import java.sql.Time; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class ViterbiLapper implements Lapper { - static final String SOURCE_NAME = "viterbi-lapper"; - - private final ViterbiLapperConfiguration config; - private Map currentStates; - private final Jdbi jdbi; - private final int lapSourceId; - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private boolean debounceScheduled; - private final Logger logger = Logger.getLogger(ViterbiLapper.class.getName()); - - public ViterbiLapper(Jdbi jdbi) { - this(jdbi, new ViterbiLapperConfiguration()); - } - - public ViterbiLapper(Jdbi jdbi, ViterbiLapperConfiguration configuration) { - this.jdbi = jdbi; - this.config = configuration; - this.currentStates = new HashMap<>(); - this.debounceScheduled = false; - - LapSourceDAO lapSourceDAO = jdbi.onDemand(LapSourceDAO.class); - - lapSourceDAO.getByName(ViterbiLapper.SOURCE_NAME).orElseThrow(); - - this.lapSourceId = lapSourceDAO.getByName(ViterbiLapper.SOURCE_NAME).get().getId(); - } - - - private ViterbiModel createViterbiModel() { - StationDAO stationDAO = jdbi.onDemand(StationDAO.class); - - // We will construct one segment for each station, which will represent its - // neighbourhood. - List stations = stationDAO.getAll(); - stations.sort(Comparator.comparing(Station::getDistanceFromStart)); - - - // *********************************** - // Build detection probability mapping - // *********************************** - Map> emissionProbabilities = new HashMap<>(); - for (int segmentNum = 0; segmentNum < stations.size(); segmentNum++) { - Map probas = new HashMap<>(); - for (int stationNum = 0; stationNum < stations.size(); stationNum++) { - int stationId = stations.get(stationNum).getId(); - if (segmentNum == stationNum) { - probas.put(stationId, this.config.SAME_STATION_DETECTION_CHANCE); - } else { - probas.put(stationId, this.config.BASE_DETECTION_CHANCE); - } - } - emissionProbabilities.put(segmentNum, probas); - } - - // ************************************ - // Build transition probability mapping - // ************************************ - Map> transitionProbabilities = new HashMap<>(); - for (int prevSegment = 0; prevSegment < stations.size(); prevSegment++) { - Map probas = new HashMap<>(); - - // a station is skipped when it is broken, or when all detections are missed - // TODO: this currently does not take into account detections by other stations - double skipStationProbability = this.config.BROKEN_STATION_PROBABILITY + Math.pow( - 1 - this.config.SAME_STATION_DETECTION_CHANCE, - this.config.EXPECTED_NUM_DETECTIONS - ); - - // calculate amount of steps this way so that backwards steps are rounded down - // and forward steps is rounded up (because we'd like to assume people run in the right direction) - int numStepsBackwards = (stations.size() - 1) / 2; - int numStepsForwards = stations.size() - 1 - numStepsBackwards; - - // This represents the odds (sameStationWeight against one) of staying on the same station - // "how much more likely is it to stay, compared to moving" - double sameStationWeight = (1 - this.config.BROKEN_STATION_PROBABILITY) * this.config.EXPECTED_NUM_DETECTIONS; - // add 2: one unit of weight for running forwards, one for running backwards - probas.put(prevSegment, sameStationWeight / (sameStationWeight + 2)); - - // transition probabilities for running forwards. - // To be precise: these probabilities should represent the probability that you will be at nextSegment - // when the next detection happens. So assuming that you cannot be detected when you are in segment 2, - // the transition probabilities towards segment 2 should always be 0. - // This is because the viterbi algorithm runs in discrete time-steps, where each step corresponds - // to a single detection (and is only vaguely related to the passage of time or distance). - double remainingProbabilityMass = 1 / (sameStationWeight + 2); - for (int i = 1; i <= numStepsForwards; i++) { - // compute next segment index - int nextSegment = Math.floorMod(prevSegment + i, stations.size()); - double proba = remainingProbabilityMass; - if (i < numStepsForwards) { - // multiply by the probability that this station was not skipped. - // When this is the final step, we do not consider the possibility of skipping anymore - // (so that probabilities add up to 1) - proba *= (1 - skipStationProbability); - } - probas.put(nextSegment, proba); - - // subtract the used amount of probability mass - remainingProbabilityMass -= proba; - } - - // transition probabilities for running backwards - // refer to above comments - remainingProbabilityMass = 1 / (sameStationWeight + 2); - for (int i = 1; i <= numStepsBackwards; i++) { - int nextSegment = Math.floorMod(prevSegment - i, stations.size()); - double proba = remainingProbabilityMass; - if (i < numStepsBackwards) { - proba *= (1 - skipStationProbability); - } - probas.put(nextSegment, proba); - remainingProbabilityMass -= proba; - } - - transitionProbabilities.put(prevSegment, probas); - } - - return new ViterbiModel<>( - stations.stream().map(Station::getId).collect(Collectors.toSet()), - IntStream.range(0, stations.size()).boxed().collect(Collectors.toSet()), - transitionProbabilities, - emissionProbabilities, - calculateStartProbabilities() - ); - } - - public Map> getProbabilities() { - Map> ret = new HashMap<>(); - for (Map.Entry entry : this.currentStates.entrySet()) { - ret.put(entry.getKey(), entry.getValue().probabilities()); - } - return ret; - } - - public Map>> getLapTimes() { - Map>> ret = new HashMap<>(); - for (Map.Entry entry : this.currentStates.entrySet()) { - ret.put(entry.getKey(), entry.getValue().lapTimestamps()); - } - return ret; - } - - private Map calculateStartProbabilities() { - StationDAO stationDAO = jdbi.onDemand(StationDAO.class); - List stations = stationDAO.getAll(); - int numStations = stations.size(); - - Map ret = new HashMap<>(); - - ret.put(0, 1.0 - (numStations - 1) * this.config.RESTART_PROBABILITY); - for (int i = 1; i < numStations; i++) { - ret.put(i, this.config.RESTART_PROBABILITY); - } - - return ret; - } - - @Override - public synchronized void handle(Detection msg) { - if (msg.getRssi() < -77) { - return; - } - if (!this.debounceScheduled) { - // TODO: this might be better as an atomic - this.debounceScheduled = true; - this.scheduler.schedule(() -> { - try { - this.calculateLaps(); - } catch (Exception e) { - logger.severe(e.getMessage()); - } - this.debounceScheduled = false; - }, this.config.DEBOUNCE_TIMEOUT, TimeUnit.SECONDS); - } - } - - public synchronized void calculateLaps() { - System.out.println("Calculating laps"); - - TeamDAO teamDAO = this.jdbi.onDemand(TeamDAO.class); - DetectionDAO detectionDAO = this.jdbi.onDemand(DetectionDAO.class); - LapDAO lapDAO = this.jdbi.onDemand(LapDAO.class); - BatonSwitchoverDAO batonSwitchoverDAO = this.jdbi.onDemand(BatonSwitchoverDAO.class); - LapSourceSwitchoverDAO lapSourceSwitchoverDAO = this.jdbi.onDemand(LapSourceSwitchoverDAO.class); - List teams = teamDAO.getAll(); - List switchovers = batonSwitchoverDAO.getAll(); - - Optional maybeFirstLapSourceSwitchover = lapSourceSwitchoverDAO.getAll().stream().filter((x) -> x.getNewLapSource() == 3).findFirst(); - Timestamp firstSwitchover = maybeFirstLapSourceSwitchover.map(LapSourceSwitchover::getTimestamp).orElse(new Timestamp(0)); - - // TODO: stream these from the database - List detections = detectionDAO.getAll(); - detections.removeIf((detection) -> detection.getRssi() < -77 || detection.getTimestamp().before(firstSwitchover)); - detections.sort(Comparator.comparing(Detection::getTimestamp)); - - // we create a viterbi model each time because the set of stations is not static - ViterbiModel viterbiModel = createViterbiModel(); - - Map> viterbis = teams.stream() - .collect(Collectors.toMap(Team::getId, _team -> new ViterbiAlgorithm<>(viterbiModel))); - - Map batonIdToTeamId = new HashMap<>(); - - int switchoverIndex = 0; - - for (Detection detection : detections) { - while (switchoverIndex < switchovers.size() && switchovers.get(switchoverIndex).getTimestamp().before(detection.getTimestamp())) { - BatonSwitchover switchover = switchovers.get(switchoverIndex); - batonIdToTeamId.put(switchover.getNewBatonId(), switchover.getTeamId()); - switchoverIndex += 1; - } - - if (batonIdToTeamId.containsKey(detection.getBatonId())) { - int teamId = batonIdToTeamId.get(detection.getBatonId()); - viterbis.get(teamId).observe(detection.getStationId(), detection.getTimestamp()); - } - } - - this.currentStates = viterbis.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getState())); - - // We made a new estimation of the lap count, now we can update the database state to match - - Map> lapsByTeam = teams.stream().collect(Collectors.toMap(Team::getId, x -> new TreeSet<>(Comparator.comparing(Lap::getTimestamp)))); - - for (Lap lap : lapDAO.getAllBySource(this.lapSourceId)) { - lapsByTeam.get(lap.getTeamId()).add(lap); - } - - for (Map.Entry entry : this.currentStates.entrySet()) { - int teamId = entry.getKey(); - TreeSet laps = lapsByTeam.get(teamId); - - ViterbiState state = entry.getValue(); - - Set currentLaps = laps.stream().map(Lap::getTimestamp).collect(Collectors.toSet()); - Set predictedLaps = state.lapTimestamps().get(state.mostLikelySegment()); - - Set toRemove = new TreeSet<>(currentLaps); - toRemove.removeAll(predictedLaps); - - Set toAdd = new TreeSet<>(predictedLaps); - toAdd.removeAll(currentLaps); - - for (Timestamp timestamp : toRemove) { - lapDAO.removeByTeamAndTimestamp(teamId, timestamp); - } - - for (Timestamp timestamp : toAdd) { - lapDAO.insert(new Lap(teamId, this.lapSourceId, timestamp)); - } - } - - System.out.println("Done calculating laps"); - } - - @Override - public void registerAPI(JerseyEnvironment jersey) { - jersey.register(new ViterbiLapperResource(this)); - } - - public ViterbiLapperConfiguration getConfig() { - return this.config; - } - - public ViterbiModel getModel() { - return this.createViterbiModel(); - } -} diff --git a/src/main/java/telraam/logic/viterbi/ViterbiLapperConfiguration.java b/src/main/java/telraam/logic/viterbi/ViterbiLapperConfiguration.java deleted file mode 100644 index 0c1c8ce..0000000 --- a/src/main/java/telraam/logic/viterbi/ViterbiLapperConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package telraam.logic.viterbi; - -public class ViterbiLapperConfiguration { - public double RESTART_PROBABILITY; // The probability that the runners will start the race in a different spot than the start/finish line (should only happen on complete restarts) - public int DEBOUNCE_TIMEOUT; // The amount of time detections are debounced for in seconds - - // probability that you will be for detection in the segment corresponding to a station - public double SAME_STATION_DETECTION_CHANCE; - - // the probability that you will be detected at a random station ("noise" detections) - public double BASE_DETECTION_CHANCE; - - // the probability that an individual station is down at any moment in time - // ~= downtime / total time. - // It is not that important that this parameter is estimated correctly (which would be very hard to do) - // it is better to interpret this as the system's eagerness to assume that a station was not working - // when trying to explain a series of detections. - public double BROKEN_STATION_PROBABILITY; - - // The amount of times we expect a runner to be detected when passing by a station, given that the station is alive. - // - // The higher this parameter, the more 'evidence' (= detections) the system will require - // in order to change the predicted location - so, the predictions will be less sensitive to noise detections - // at different stations. - // Consequently, it is better to under-estimate this parameter than to over-estimate it. - public double EXPECTED_NUM_DETECTIONS; - - public ViterbiLapperConfiguration() { - this.RESTART_PROBABILITY = 0.001; - this.DEBOUNCE_TIMEOUT = 10; - - // ballpark estimates extracted from test event data - // IMPORTANT: these numbers are only valid assuming that detection are filtered for rssi >=-70 - // (ask @iasoon for details) - this.SAME_STATION_DETECTION_CHANCE = 0.90; - this.BASE_DETECTION_CHANCE = 0.10; - this.EXPECTED_NUM_DETECTIONS = 3; - - - this.BROKEN_STATION_PROBABILITY = 0.01; - } -} diff --git a/src/main/java/telraam/logic/viterbi/ViterbiLapperResource.java b/src/main/java/telraam/logic/viterbi/ViterbiLapperResource.java deleted file mode 100644 index 3b1de23..0000000 --- a/src/main/java/telraam/logic/viterbi/ViterbiLapperResource.java +++ /dev/null @@ -1,61 +0,0 @@ -package telraam.logic.viterbi; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import telraam.logic.viterbi.algorithm.ViterbiModel; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.sql.Timestamp; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Path("/lappers/viterbi") -@Api("/lappers/viterbi") -@Produces(MediaType.APPLICATION_JSON) -public class ViterbiLapperResource { - private final ViterbiLapper lapper; - - public ViterbiLapperResource(ViterbiLapper lapper) { - this.lapper = lapper; - } - - @GET - @Path("/probabilities") - @ApiOperation(value = "Get lapper position probabilities") - public Map> getProbabilities() { - return this.lapper.getProbabilities(); - } - - @GET - @Path("/lap-times") - @ApiOperation(value = "Get lapper estimated laps") - public Map>> getLapTimes() { - return this.lapper.getLapTimes(); - } - - @GET - @Path("/configuration") - @ApiOperation(value = "Get lapper configuration") - public ViterbiLapperConfiguration getConfiguration() { - return this.lapper.getConfig(); - } - - @GET - @Path("/model") - @ApiOperation(value = "Get Viterbi model") - public ViterbiModel getModel() { - return this.lapper.getModel(); - } - - @GET - @Path("/recalculate") - @ApiOperation(value = "Recalculate Viterbi rounds") - public String recalculateRounds() { - this.lapper.calculateLaps(); - return "Recalculated rounds"; - } -} diff --git a/src/main/java/telraam/logic/viterbi/algorithm/InvalidParameterException.java b/src/main/java/telraam/logic/viterbi/algorithm/InvalidParameterException.java deleted file mode 100644 index 632bf2d..0000000 --- a/src/main/java/telraam/logic/viterbi/algorithm/InvalidParameterException.java +++ /dev/null @@ -1,7 +0,0 @@ -package telraam.logic.viterbi.algorithm; - -public class InvalidParameterException extends RuntimeException { - public InvalidParameterException(String s) { - super(s); - } -} diff --git a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiAlgorithm.java b/src/main/java/telraam/logic/viterbi/algorithm/ViterbiAlgorithm.java deleted file mode 100644 index 2f4e82f..0000000 --- a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiAlgorithm.java +++ /dev/null @@ -1,127 +0,0 @@ -package telraam.logic.viterbi.algorithm; - -import io.swagger.models.auth.In; - -import java.sql.Time; -import java.sql.Timestamp; -import java.util.*; - -/** - * The class performing the Viterbi algorithm. - * @param The type of the observations. - */ -public class ViterbiAlgorithm { - private final ViterbiModel model; - - private ViterbiState lastState; - - public ViterbiAlgorithm(ViterbiModel viterbiModel) { - this.model = viterbiModel; - - this.verifyProbabilities(); - - // Set up the initial probabilities - int numSegments = this.model.getHiddenStates().size(); - Map probabilities = new HashMap<>(); - Map previousSegments = new HashMap<>(); - Map> lapTimestamps = new HashMap<>(); - - for (Map.Entry entry : viterbiModel.getStartProbabilities().entrySet()) { - probabilities.put(entry.getKey(), entry.getValue()); - previousSegments.put(entry.getKey(), 0); - lapTimestamps.put(entry.getKey(), new TreeSet<>()); - } - - this.lastState = new ViterbiState(probabilities, previousSegments, lapTimestamps); - } - - /** - * Verify that the given probabilities are valid. - * @throws InvalidParameterException If the probabilities were not valid. - */ - private void verifyProbabilities() { - if (!this.model.getTransitionProbabilities().keySet().equals(this.model.getHiddenStates())) { - throw new InvalidParameterException("Invalid key set for transition probabilities"); - } - - for (Integer state : this.model.getHiddenStates()) { - if (!this.model.getTransitionProbabilities().get(state).keySet().equals(this.model.getHiddenStates())) { - throw new InvalidParameterException("Invalid key set for transition probabilities for state " + state); - } - } - - if (!this.model.getEmitProbabilities().keySet().equals(this.model.getHiddenStates())) { - throw new InvalidParameterException("Invalid key set for emission probabilities: " + this.model.getEmitProbabilities().keySet() + " != " + this.model.getHiddenStates()); - } - - for (Integer state : this.model.getHiddenStates()) { - if (!this.model.getTransitionProbabilities().get(state).keySet().equals(this.model.getHiddenStates())) { - throw new InvalidParameterException( - "Invalid key set for emission probabilities for state " + - state + - ": " + - this.model.getTransitionProbabilities().get(state).keySet() + - " != " + - this.model.getObservations() - ); - } - } - } - - /** - * Handle an observation. - * @param observation The observation to process. - * @param observationTimestamp The timestamp when this observation was made - */ - public void observe(O observation, Timestamp observationTimestamp) { - int numSegments = this.model.getHiddenStates().size(); - Map probabilities = new HashMap<>(); - Map previousSegments = new HashMap<>(); - Map> lapTimestamps = new HashMap<>(); - - for (int nextSegment = 0; nextSegment < numSegments; nextSegment++) { - probabilities.put(nextSegment, 0.0); - for (int previousSegment = 0; previousSegment < numSegments; previousSegment++) { - double probability = this.lastState.probabilities().get(previousSegment) * - this.model.getTransitionProbabilities().get(previousSegment).get(nextSegment) * - this.model.getEmitProbabilities().get(nextSegment).get(observation); - if (probabilities.get(nextSegment) < probability) { - probabilities.put(nextSegment, probability); - previousSegments.put(nextSegment, previousSegment); - - int half = numSegments / 2; - // Dit is het algoritme van De Voerstreek - int delta = half - (half - (nextSegment - previousSegment)) % numSegments; - - Set newTimestamps = new TreeSet<>(this.lastState.lapTimestamps().get(previousSegment)); - - if (delta > 0 && previousSegment > nextSegment) { - // forward wrap-around - newTimestamps.add(observationTimestamp); - } else if (delta < 0 && previousSegment < nextSegment) { - // backwards wrap-around - Optional highestTimestamp = newTimestamps.stream().max(Timestamp::compareTo); - highestTimestamp.ifPresent(newTimestamps::remove); - } - lapTimestamps.put(nextSegment, newTimestamps); - } - } - } - - // normalize probabilities - double sum = probabilities.values().stream().reduce(0.0, Double::sum); - for (int i = 0; i < numSegments; i++) { - probabilities.put(i, probabilities.get(i) / sum); - } - - this.lastState = new ViterbiState(probabilities, previousSegments, lapTimestamps); - } - - /** - * Get the current state of the Viterbi algorithm. - * @return The last Result. - */ - public ViterbiState getState() { - return this.lastState; - } -} diff --git a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiModel.java b/src/main/java/telraam/logic/viterbi/algorithm/ViterbiModel.java deleted file mode 100644 index edda291..0000000 --- a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package telraam.logic.viterbi.algorithm; - -import java.util.Map; -import java.util.Set; - -public record ViterbiModel(Set observations, Set hiddenStates, - Map> transitionProbabilities, - Map> emitProbabilities, - Map startProbabilities) { - - public Set getObservations() { - return observations; - } - - public Set getHiddenStates() { - return hiddenStates; - } - - public Map> getTransitionProbabilities() { - return transitionProbabilities; - } - - public Map> getEmitProbabilities() { - return emitProbabilities; - } - - public Map getStartProbabilities() { - return startProbabilities; - } -} diff --git a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiState.java b/src/main/java/telraam/logic/viterbi/algorithm/ViterbiState.java deleted file mode 100644 index ada5d6b..0000000 --- a/src/main/java/telraam/logic/viterbi/algorithm/ViterbiState.java +++ /dev/null @@ -1,25 +0,0 @@ -package telraam.logic.viterbi.algorithm; - -import java.sql.Timestamp; -import java.util.Map; -import java.util.Set; - -/** - * Helper class to store steps in the Viterbi algorithm. - */ -public record ViterbiState(Map probabilities, Map previousStates, Map> lapTimestamps) { - /** - * Get the most likely state to be in, in this Result - * - * @return The state that is most likely. - */ - public Integer mostLikelySegment() { - int mostLikelySegment = 0; - for (int i : probabilities.keySet()) { - if (this.probabilities.get(i) > this.probabilities.get(mostLikelySegment)) { - mostLikelySegment = i; - } - } - return mostLikelySegment; - } -} diff --git a/src/main/java/telraam/monitoring/BatonDetectionManager.java b/src/main/java/telraam/monitoring/BatonDetectionManager.java new file mode 100644 index 0000000..f48dafd --- /dev/null +++ b/src/main/java/telraam/monitoring/BatonDetectionManager.java @@ -0,0 +1,68 @@ +package telraam.monitoring; + +import telraam.database.daos.BatonSwitchoverDAO; +import telraam.database.daos.DetectionDAO; +import telraam.database.daos.TeamDAO; +import telraam.database.models.Team; +import telraam.monitoring.models.BatonDetection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BatonDetectionManager { + private final DetectionDAO detectionDAO; + private final TeamDAO teamDAO; + private final BatonSwitchoverDAO batonSwitchoverDAO; + + // Map of a baton id to it's detections + private final Map> batonDetectionMap = new HashMap<>(); + private Integer handledDetectionId = 0; + + public BatonDetectionManager(DetectionDAO detectionDAO, TeamDAO teamDAO, BatonSwitchoverDAO batonSwitchoverDAO) { + this.detectionDAO = detectionDAO; + this.teamDAO = teamDAO; + this.batonSwitchoverDAO = batonSwitchoverDAO; + } + + public Map> getBatonDetections() { + var detections = detectionDAO.getSinceId(handledDetectionId, Integer.MAX_VALUE); + var batonSwitchOvers = batonSwitchoverDAO.getAll(); + // Map of batonIds to team info + var teamMap = new HashMap(); + var teams = teamDAO.getAll(); + teams.forEach(t -> teamMap.put(t.getId(), t)); + detections.forEach(d -> { + Map batonTeamMap = new HashMap<>(); + batonSwitchOvers.forEach(b -> { + if (b.getTimestamp().after(d.getTimestamp())) { + return; + } + if (b.getPreviousBatonId() != null && batonTeamMap.containsKey(b.getPreviousBatonId())) { + if (batonTeamMap.get(b.getPreviousBatonId()).equals(b.getTeamId())) { + batonTeamMap.remove(b.getPreviousBatonId()); + } + } + if (b.getNewBatonId() != null) { + batonTeamMap.put(b.getNewBatonId(), b.getTeamId()); + } + }); + if (!batonTeamMap.containsKey(d.getBatonId())) { + return; + } + if (d.getId() > handledDetectionId) { + handledDetectionId = d.getId(); + } + Integer batonId = d.getBatonId(); + if (!batonDetectionMap.containsKey(batonId)) { + batonDetectionMap.put(batonId, new ArrayList<>()); + } + var batonDetections = batonDetectionMap.get(batonId); + var team = teamMap.get(batonTeamMap.get(batonId)); + var batonDetection = new BatonDetection(Math.toIntExact(d.getTimestamp().getTime() / 1000), d.getRssi(), batonId /* FIXME: BatonDetection expects a teamId here? */, d.getStationId(), team.getName()); + batonDetections.add(batonDetection); + }); + return batonDetectionMap; + } +} diff --git a/src/main/java/telraam/monitoring/BatonStatusHolder.java b/src/main/java/telraam/monitoring/BatonStatusHolder.java new file mode 100644 index 0000000..ea2d39c --- /dev/null +++ b/src/main/java/telraam/monitoring/BatonStatusHolder.java @@ -0,0 +1,128 @@ +package telraam.monitoring; + +import telraam.database.daos.BatonDAO; +import telraam.database.daos.DetectionDAO; +import telraam.database.models.Baton; +import telraam.database.models.Detection; +import telraam.monitoring.models.BatonStatus; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public class BatonStatusHolder { + // Map from batonMac to batonStatus + private final HashMap batonStatusMap = new HashMap<>(); + private final HashMap batonIdToMac = new HashMap<>(); + + private final BatonDAO batonDAO; + private final DetectionDAO detectionDAO; + + public BatonStatusHolder(BatonDAO batonDAO, DetectionDAO detectionDAO) { + this.batonDAO = batonDAO; + this.detectionDAO = detectionDAO; + } + + private BatonStatus getStatusForBaton(String batonMac) { + BatonStatus batonStatus = batonStatusMap.get(batonMac); + if (batonStatus == null) { + Optional optionalBaton = batonDAO.getByMac(batonMac); + if (optionalBaton.isEmpty()) { + return null; + } + Baton baton = optionalBaton.get(); + + batonStatus = new BatonStatus( + baton.getMac().toLowerCase(), + baton.getId(), + baton.getName(), + 0, + 0, + false, + null, + -1 + ); + batonStatusMap.put(baton.getMac().toLowerCase(), batonStatus); + } + return batonStatus; + + } + + public List GetAllBatonStatuses() { + // For each baton, fetch latest detection + var batons = batonDAO.getAll(); + for (Baton baton : batons) { + var batonStatus = GetBatonStatus(baton.getId()); + var detection = detectionDAO.latestDetectionByBatonId(baton.getId(), batonStatus.getLastSeen() == null ? Timestamp.from(Instant.ofEpochSecond(0)) : batonStatus.getLastSeen()); + detection.ifPresent(this::updateState); + } + return new ArrayList<>(batonStatusMap.values()); + } + + private void updateState(Detection msg) { + BatonStatus batonStatus = GetBatonStatus(msg.getBatonId()); + if (batonStatus == null) { + batonStatus = createBatonStatus(msg.getBatonId()); + batonStatusMap.put(batonStatus.getMac(), batonStatus); + } + if (batonStatus.getLastSeen() == null) { + batonStatus.setLastSeen(msg.getTimestamp()); + } + if (batonStatus.getLastSeen().after(msg.getTimestamp())) { + return; + } + if (msg.getUptimeMs() < batonStatus.getUptime() - 3000) { + batonStatus.setRebooted(true); + } + batonStatus.setLastSeenSecondsAgo((System.currentTimeMillis() - msg.getTimestamp().getTime()) / 1000); + batonStatus.setLastSeen(msg.getTimestamp()); + batonStatus.setUptime(msg.getUptimeMs() / 1000); + batonStatus.setBattery(msg.getBattery()); + batonStatus.setLastDetectedAtStation(msg.getStationId()); + } + + public BatonStatus GetBatonStatus(Integer batonId) { + if (!batonIdToMac.containsKey(batonId)) { + var baton = batonDAO.getById(batonId); + baton.ifPresent(value -> batonIdToMac.put(batonId, value.getMac().toLowerCase())); + } + String batonMac = batonIdToMac.get(batonId); + return getStatusForBaton(batonMac); + } + + public BatonStatus createBatonStatus(Integer batonId) { + String batonMac = batonIdToMac.get(batonId); + if (batonMac != null) { + return getStatusForBaton(batonMac); + } + var baton = batonDAO.getById(batonId); + if (baton.isEmpty()) { + // Non-existing baton + return null; + } + BatonStatus batonStatus = new BatonStatus( + baton.get().getMac().toLowerCase(), + baton.get().getId(), + baton.get().getName(), + 0, + 0, + false, + null, + -1 + ); + batonStatusMap.put(batonStatus.getMac(), batonStatus); + batonIdToMac.put(batonId, batonStatus.getMac()); + return batonStatus; + } + + public void resetRebooted(int batonId) { + var batonStatus = GetBatonStatus(batonId); + if (batonStatus == null) { + return; + } + batonStatus.setRebooted(false); + } +} diff --git a/src/main/java/telraam/monitoring/StationDetectionManager.java b/src/main/java/telraam/monitoring/StationDetectionManager.java new file mode 100644 index 0000000..931883f --- /dev/null +++ b/src/main/java/telraam/monitoring/StationDetectionManager.java @@ -0,0 +1,36 @@ +package telraam.monitoring; + +import telraam.database.daos.DetectionDAO; +import telraam.database.daos.StationDAO; +import telraam.database.models.Detection; +import telraam.database.models.Station; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class StationDetectionManager { + private final DetectionDAO detectionDAO; + + private final StationDAO stationDAO; + + public StationDetectionManager(DetectionDAO detectionDAO, StationDAO stationDAO) { + this.detectionDAO = detectionDAO; + this.stationDAO = stationDAO; + } + + public Map timeSinceLastDetectionPerStation() { + List stationIdList = stationDAO.getAll().stream().map(Station::getId).toList(); + Map stationIdToTimeSinceLatestDetection = new HashMap<>(); + for (Integer stationId : stationIdList) { + Optional maybeDetection = this.detectionDAO.latestDetectionByStationId(stationId); + Optional maybeStation = this.stationDAO.getById(stationId); + if (maybeDetection.isPresent() && maybeStation.isPresent()) { + long time = maybeDetection.get().getTimestamp().getTime(); + stationIdToTimeSinceLatestDetection.put(maybeStation.get().getName(), (System.currentTimeMillis() - time) / 1000); + } + } + return stationIdToTimeSinceLatestDetection; + } +} diff --git a/src/main/java/telraam/monitoring/models/BatonDetection.java b/src/main/java/telraam/monitoring/models/BatonDetection.java new file mode 100644 index 0000000..29eee97 --- /dev/null +++ b/src/main/java/telraam/monitoring/models/BatonDetection.java @@ -0,0 +1,22 @@ +package telraam.monitoring.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +public class BatonDetection { + @JsonProperty("detected_time") + private Integer detectionTime; + @JsonProperty("rssi") + private Integer rssi; + @JsonProperty("team_id") + private Integer teamId; + @JsonProperty("station_id") + private Integer stationId; + @JsonProperty("team_name") + private String teamName; +} diff --git a/src/main/java/telraam/monitoring/models/BatonStatus.java b/src/main/java/telraam/monitoring/models/BatonStatus.java new file mode 100644 index 0000000..c719098 --- /dev/null +++ b/src/main/java/telraam/monitoring/models/BatonStatus.java @@ -0,0 +1,44 @@ +package telraam.monitoring.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.sql.Timestamp; + +@Getter +@Setter +public class BatonStatus { + + private String mac; + private Integer id; + private String name; + private Float battery; + private Long uptime; // Uptime in seconds + private Boolean rebooted; + @JsonProperty("time_since_seen") + private Long lastSeenSecondsAgo; + @JsonIgnore + private Timestamp lastSeen; + @JsonProperty("last_detected_at_station") + private Integer lastDetectedAtStation; + + public BatonStatus(String mac, Integer id, String name, float battery, long uptime, boolean rebooted, Timestamp lastSeen, Integer lastDetectedAtStation) { + this.mac = mac; + this.id = id; + this.name = name; + this.battery = battery; + this.uptime = uptime; + this.rebooted = rebooted; + this.lastSeen = lastSeen; + this.lastSeenSecondsAgo = lastSeen != null ? (System.currentTimeMillis() - lastSeen.getTime()) / 1000 : null; + this.lastDetectedAtStation = lastDetectedAtStation; + } + + @Override + public String toString() { + return String.format("BatonStatus{mac='%s', id=%d, name='%s', battery=%f, uptime=%d, rebooted=%b, lastSeen=%s, lastSeenSecondsAgo=%d, lastDetectedAtStation=%d}", + mac, id, name, battery, uptime, rebooted, lastSeen, lastSeenSecondsAgo, lastDetectedAtStation); + } +} diff --git a/src/main/java/telraam/monitoring/models/LapCountForTeam.java b/src/main/java/telraam/monitoring/models/LapCountForTeam.java new file mode 100644 index 0000000..3e8335e --- /dev/null +++ b/src/main/java/telraam/monitoring/models/LapCountForTeam.java @@ -0,0 +1,16 @@ +package telraam.monitoring.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class LapCountForTeam { + + @JsonProperty("team_name") + private String teamName; + + @JsonProperty("lap_counts") + private Map lapCounts; +} diff --git a/src/main/java/telraam/monitoring/models/TeamLapInfo.java b/src/main/java/telraam/monitoring/models/TeamLapInfo.java new file mode 100644 index 0000000..b244c27 --- /dev/null +++ b/src/main/java/telraam/monitoring/models/TeamLapInfo.java @@ -0,0 +1,20 @@ +package telraam.monitoring.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class TeamLapInfo { + + @JsonProperty("lap_time") + private long lapTime; + + @JsonProperty("timestamp") + private long timestamp; + + @JsonProperty("team_id") + private int teamId; + + @JsonProperty("team_name") + private String teamName; +} diff --git a/src/main/java/telraam/station/Fetcher.java b/src/main/java/telraam/station/Fetcher.java index cf371f8..753474a 100644 --- a/src/main/java/telraam/station/Fetcher.java +++ b/src/main/java/telraam/station/Fetcher.java @@ -1,174 +1,14 @@ package telraam.station; -import org.jdbi.v3.core.Jdbi; -import telraam.database.daos.BatonDAO; -import telraam.database.daos.DetectionDAO; -import telraam.database.daos.StationDAO; -import telraam.database.models.Baton; -import telraam.database.models.Detection; -import telraam.database.models.Station; -import telraam.logic.Lapper; - -import java.io.IOException; -import java.net.ConnectException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpConnectTimeoutException; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.sql.Timestamp; -import java.time.Duration; -import java.util.*; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -public class Fetcher { - private final Set lappers; - private Station station; - - private final BatonDAO batonDAO; - private final DetectionDAO detectionDAO; - private final StationDAO stationDAO; - - private final HttpClient client = HttpClient.newHttpClient(); - private final Logger logger = Logger.getLogger(Fetcher.class.getName()); - +public interface Fetcher { //Timeout to wait for before sending the next request after an error. - private final static int ERROR_TIMEOUT_MS = 2000; + int ERROR_TIMEOUT_MS = 2000; //Timeout for a request to a station. - private final static int REQUEST_TIMEOUT_S = 10; + int REQUEST_TIMEOUT_S = 10; //Full batch size, if this number of detections is reached, more are probably available immediately. - private final static int FULL_BATCH_SIZE = 1000; + int FULL_BATCH_SIZE = 1000; //Timeout when result has less than a full batch of detections. - private final static int IDLE_TIMEOUT_MS = 4000; // Wait 4 seconds - - - public Fetcher(Jdbi database, Station station, Set lappers) { - this.batonDAO = database.onDemand(BatonDAO.class); - this.detectionDAO = database.onDemand(DetectionDAO.class); - this.stationDAO = database.onDemand(StationDAO.class); - this.lappers = lappers; - - this.station = station; - } - - public void fetch() { - logger.info("Running Fetcher for station(" + this.station.getId() + ")"); - JsonBodyHandler bodyHandler = new JsonBodyHandler<>(RonnyResponse.class); - - while (true) { - //Update the station to account for possible changes in the database - this.stationDAO.getById(station.getId()).ifPresentOrElse( - station -> this.station = station, - () -> this.logger.severe("Can't update station from database.") - ); - - //Get last detection id - int lastDetectionId = 0; - Optional lastDetection = detectionDAO.latestDetectionByStationId(this.station.getId()); - if (lastDetection.isPresent()) { - lastDetectionId = lastDetection.get().getRemoteId(); - } - - //Create URL - URI url; - try { - url = new URI(station.getUrl() + "/detections/" + lastDetectionId); - } catch (URISyntaxException ex) { - this.logger.severe(ex.getMessage()); - try { - Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); - } catch (InterruptedException e) { - logger.severe(e.getMessage()); - } - continue; - } - - //Create request - HttpRequest request; - try { - request = HttpRequest.newBuilder() - .uri(url) - .version(HttpClient.Version.HTTP_1_1) - .timeout(Duration.ofSeconds(Fetcher.REQUEST_TIMEOUT_S)) - .build(); - } catch (IllegalArgumentException e) { - logger.severe(e.getMessage()); - try { - Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); - } catch (InterruptedException ex) { - logger.severe(ex.getMessage()); - } - continue; - } - - //Do request - HttpResponse> response; - try { - try { - response = this.client.send(request, bodyHandler); - } catch (ConnectException | HttpConnectTimeoutException ex) { - this.logger.severe("Could not connect to " + request.uri()); - Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); - continue; - } catch (IOException e) { - logger.severe(e.getMessage()); - Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); - continue; - } - } catch (InterruptedException e) { - logger.severe(e.getMessage()); - continue; - } - - //Check response state - if (response.statusCode() != 200) { - this.logger.warning( - "Unexpected status code(" + response.statusCode() + ") when requesting " + url + " for station(" + this.station.getName() + ")" - ); - continue; - } - - //Fetch all batons and create a map by batonMAC - Map baton_mac_map = batonDAO.getAll().stream() - .collect(Collectors.toMap(b -> b.getMac().toUpperCase(), Function.identity())); - - //Insert detections - List new_detections = new ArrayList<>(); - List detections = response.body().get().detections; - for (RonnyDetection detection : detections) { - if (baton_mac_map.containsKey(detection.mac.toUpperCase())) { - var baton = baton_mac_map.get(detection.mac.toUpperCase()); - new_detections.add(new Detection( - baton.getId(), - station.getId(), - detection.rssi, - detection.battery, - detection.uptimeMs, - detection.id, - new Timestamp((long) (detection.detectionTimestamp * 1000)), - new Timestamp(System.currentTimeMillis()) - )); - } - } - if (!new_detections.isEmpty()) { - detectionDAO.insertAll(new_detections); - new_detections.forEach((detection) -> lappers.forEach((lapper) -> lapper.handle(detection))); - } - - this.logger.finer("Fetched " + detections.size() + " detections from " + station.getName() + ", Saved " + new_detections.size()); + int IDLE_TIMEOUT_MS = 4000; // Wait 4 seconds - //If few detections are retrieved from the station, wait for some time. - if (detections.size() < Fetcher.FULL_BATCH_SIZE) { - try { - Thread.sleep(Fetcher.IDLE_TIMEOUT_MS); - } catch (InterruptedException e) { - logger.severe(e.getMessage()); - } - } - } - } -} \ No newline at end of file + void fetch(); +} diff --git a/src/main/java/telraam/station/FetcherFactory.java b/src/main/java/telraam/station/FetcherFactory.java new file mode 100644 index 0000000..b3cb487 --- /dev/null +++ b/src/main/java/telraam/station/FetcherFactory.java @@ -0,0 +1,42 @@ +package telraam.station; + +import org.jdbi.v3.core.Jdbi; +import telraam.database.models.Station; +import telraam.logic.lapper.Lapper; +import telraam.logic.positioner.Positioner; +import telraam.station.http.HTTPFetcher; +import telraam.station.websocket.WebsocketFetcher; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Set; +import java.util.logging.Logger; + +public class FetcherFactory { + private final Logger logger = Logger.getLogger(FetcherFactory.class.getName()); + private final Jdbi database; + private final Set lappers; + private final Set positioners; + public FetcherFactory(Jdbi database, Set lappers, Set positioners) { + this.database = database; + this.lappers = lappers; + this.positioners = positioners; + } + + public Fetcher create(Station station) { + try { + URI stationURI = new URI(station.getUrl()); + return switch (stationURI.getScheme()) { + case "ws" -> new WebsocketFetcher(database, station, lappers, positioners); + case "http" -> new HTTPFetcher(database, station, lappers, positioners); + default -> { + logger.severe(String.format("%s is not a valid scheme for a station", stationURI.getScheme())); + yield null; + } + }; + } catch (URISyntaxException e) { + logger.severe(String.format("Failed to parse station URI: %s", e.getMessage())); + } + return null; + } +} diff --git a/src/main/java/telraam/station/http/HTTPFetcher.java b/src/main/java/telraam/station/http/HTTPFetcher.java new file mode 100644 index 0000000..c296efd --- /dev/null +++ b/src/main/java/telraam/station/http/HTTPFetcher.java @@ -0,0 +1,174 @@ +package telraam.station.http; + +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.BatonDAO; +import telraam.database.daos.DetectionDAO; +import telraam.database.daos.StationDAO; +import telraam.database.models.Baton; +import telraam.database.models.Detection; +import telraam.database.models.Station; +import telraam.logic.lapper.Lapper; +import telraam.logic.positioner.Positioner; +import telraam.station.Fetcher; +import telraam.station.models.RonnyDetection; +import telraam.station.models.RonnyResponse; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.Timestamp; +import java.time.Duration; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class HTTPFetcher implements Fetcher { + private final Set lappers; + private final Set positioners; + private Station station; + + private final BatonDAO batonDAO; + private final DetectionDAO detectionDAO; + private final StationDAO stationDAO; + + private final HttpClient client = HttpClient.newHttpClient(); + private final Logger logger = Logger.getLogger(Fetcher.class.getName()); + + + public HTTPFetcher(Jdbi database, Station station, Set lappers, Set positioners) { + this.batonDAO = database.onDemand(BatonDAO.class); + this.detectionDAO = database.onDemand(DetectionDAO.class); + this.stationDAO = database.onDemand(StationDAO.class); + + this.lappers = lappers; + this.positioners = positioners; + this.station = station; + } + + public void fetch() { + logger.info("Running Fetcher for station(" + this.station.getId() + ")"); + JsonBodyHandler bodyHandler = new JsonBodyHandler<>(RonnyResponse.class); + + while (true) { + //Update the station to account for possible changes in the database + this.stationDAO.getById(station.getId()).ifPresentOrElse( + station -> this.station = station, + () -> this.logger.severe("Can't update station from database.") + ); + + //Get last detection id + int lastDetectionId = 0; + Optional lastDetection = detectionDAO.latestDetectionByStationId(this.station.getId()); + if (lastDetection.isPresent()) { + lastDetectionId = lastDetection.get().getRemoteId(); + } + + //Create URL + URI url; + try { + url = new URI(station.getUrl() + "/detections/" + lastDetectionId); + } catch (URISyntaxException ex) { + this.logger.severe(ex.getMessage()); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + continue; + } + + //Create request + HttpRequest request; + try { + request = HttpRequest.newBuilder() + .uri(url) + .version(HttpClient.Version.HTTP_1_1) + .timeout(Duration.ofSeconds(Fetcher.REQUEST_TIMEOUT_S)) + .build(); + } catch (IllegalArgumentException e) { + logger.severe(e.getMessage()); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException ex) { + logger.severe(ex.getMessage()); + } + continue; + } + + //Do request + HttpResponse> response; + try { + try { + response = this.client.send(request, bodyHandler); + } catch (ConnectException | HttpConnectTimeoutException ex) { + this.logger.severe("Could not connect to " + request.uri()); + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + continue; + } catch (IOException e) { + logger.severe(e.getMessage()); + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + continue; + } + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + continue; + } + + //Check response state + if (response.statusCode() != 200) { + this.logger.warning( + "Unexpected status code(" + response.statusCode() + ") when requesting " + url + " for station(" + this.station.getName() + ")" + ); + continue; + } + + //Fetch all batons and create a map by batonMAC + Map baton_mac_map = batonDAO.getAll().stream() + .collect(Collectors.toMap(b -> b.getMac().toUpperCase(), Function.identity())); + + //Insert detections + List new_detections = new ArrayList<>(); + List detections = response.body().get().detections; + for (RonnyDetection detection : detections) { + if (baton_mac_map.containsKey(detection.mac.toUpperCase())) { + var baton = baton_mac_map.get(detection.mac.toUpperCase()); + new_detections.add(new Detection( + baton.getId(), + station.getId(), + detection.rssi, + detection.battery, + detection.uptimeMs, + detection.id, + new Timestamp((long) (detection.detectionTimestamp * 1000)), + new Timestamp(System.currentTimeMillis()) + )); + } + } + if (!new_detections.isEmpty()) { + detectionDAO.insertAll(new_detections); + new_detections.forEach((detection) -> { + lappers.forEach((lapper) -> lapper.handle(detection)); + positioners.forEach((positioner) -> positioner.handle(detection)); + }); + } + + this.logger.finer("Fetched " + detections.size() + " detections from " + station.getName() + ", Saved " + new_detections.size()); + + //If few detections are retrieved from the station, wait for some time. + if (detections.size() < Fetcher.FULL_BATCH_SIZE) { + try { + Thread.sleep(Fetcher.IDLE_TIMEOUT_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/telraam/station/JsonBodyHandler.java b/src/main/java/telraam/station/http/JsonBodyHandler.java similarity index 90% rename from src/main/java/telraam/station/JsonBodyHandler.java rename to src/main/java/telraam/station/http/JsonBodyHandler.java index f9ddd04..462039f 100644 --- a/src/main/java/telraam/station/JsonBodyHandler.java +++ b/src/main/java/telraam/station/http/JsonBodyHandler.java @@ -1,4 +1,4 @@ -package telraam.station; +package telraam.station.http; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,18 +17,12 @@ public JsonBodyHandler(Class targetClass) { this.targetClass = targetClass; } - @Override - public HttpResponse.BodySubscriber> apply(HttpResponse.ResponseInfo responseInfo) { - return asJSON(this.targetClass); - } - - public static HttpResponse.BodySubscriber> asJSON(Class targetType) { HttpResponse.BodySubscriber upstream = HttpResponse.BodySubscribers.ofInputStream(); return HttpResponse.BodySubscribers.mapping( - upstream, - inputStream -> toSupplierOfType(inputStream, targetType)); + upstream, + inputStream -> toSupplierOfType(inputStream, targetType)); } public static Supplier toSupplierOfType(InputStream inputStream, Class targetType) { @@ -40,4 +34,9 @@ public static Supplier toSupplierOfType(InputStream inputStream, Class } }; } -} \ No newline at end of file + + @Override + public HttpResponse.BodySubscriber> apply(HttpResponse.ResponseInfo responseInfo) { + return asJSON(this.targetClass); + } +} diff --git a/src/main/java/telraam/station/RonnyDetection.java b/src/main/java/telraam/station/models/RonnyDetection.java similarity index 90% rename from src/main/java/telraam/station/RonnyDetection.java rename to src/main/java/telraam/station/models/RonnyDetection.java index 40689b8..234a50d 100644 --- a/src/main/java/telraam/station/RonnyDetection.java +++ b/src/main/java/telraam/station/models/RonnyDetection.java @@ -1,4 +1,4 @@ -package telraam.station; +package telraam.station.models; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/telraam/station/RonnyResponse.java b/src/main/java/telraam/station/models/RonnyResponse.java similarity index 87% rename from src/main/java/telraam/station/RonnyResponse.java rename to src/main/java/telraam/station/models/RonnyResponse.java index b2cf9ef..63e1382 100644 --- a/src/main/java/telraam/station/RonnyResponse.java +++ b/src/main/java/telraam/station/models/RonnyResponse.java @@ -1,12 +1,12 @@ -package telraam.station; - -import java.util.List; +package telraam.station.models; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + public class RonnyResponse { public List detections; @JsonProperty("station_id") public String stationRonnyName; -} +} \ No newline at end of file diff --git a/src/main/java/telraam/station/websocket/WebsocketClient.java b/src/main/java/telraam/station/websocket/WebsocketClient.java new file mode 100644 index 0000000..7767258 --- /dev/null +++ b/src/main/java/telraam/station/websocket/WebsocketClient.java @@ -0,0 +1,85 @@ +package telraam.station.websocket; + +import jakarta.websocket.*; +import org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException; + +import java.net.URI; + +@ClientEndpoint +public class WebsocketClient { + public interface MessageHandler { + void handleMessage(String message); + } + public interface onStateChangeHandler { + void handleChange(); + } + + private URI endpoint; + private Session session = null; + private MessageHandler messageHandler; + private onStateChangeHandler onOpenHandler; + private onStateChangeHandler onCloseHandler; + + public WebsocketClient(URI endpointURI) throws RuntimeException { + this.endpoint = endpointURI; + } + + public void listen() throws RuntimeException { + try { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + container.setDefaultMaxTextMessageBufferSize(100 * 1048576); // 100Mb + container.setDefaultMaxSessionIdleTimeout(60000); + container.connectToServer(this, endpoint); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @OnError + public void onError(Session session, Throwable error) throws Throwable { + if (error instanceof WebSocketTimeoutException) { + return; + } + throw error; + } + + @OnOpen + public void onOpen(Session session) { + this.session = session; + if (this.onOpenHandler != null) { + this.onOpenHandler.handleChange(); + } + } + + @OnClose + public void onClose(Session userSession, CloseReason reason) { + this.session = null; + if (this.onCloseHandler != null) { + this.onCloseHandler.handleChange(); + } + } + + @OnMessage + public void onMessage(String message) { + if (this.messageHandler != null) { + this.messageHandler.handleMessage(message); + } + } + + public void addOnOpenHandler(onStateChangeHandler openHandler) { + this.onOpenHandler = openHandler; + } + + public void addOnCloseHandler(onStateChangeHandler openHandler) { + this.onCloseHandler = openHandler; + } + + public void addMessageHandler(MessageHandler msgHandler) { + this.messageHandler = msgHandler; + } + + public void sendMessage(String message) { + + this.session.getAsyncRemote().sendText(message); + } +} diff --git a/src/main/java/telraam/station/websocket/WebsocketFetcher.java b/src/main/java/telraam/station/websocket/WebsocketFetcher.java new file mode 100644 index 0000000..3d49016 --- /dev/null +++ b/src/main/java/telraam/station/websocket/WebsocketFetcher.java @@ -0,0 +1,164 @@ +package telraam.station.websocket; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import com.fasterxml.jackson.databind.ObjectMapper; +import telraam.database.daos.BatonDAO; +import telraam.database.daos.DetectionDAO; +import telraam.database.daos.StationDAO; +import telraam.database.models.Detection; +import telraam.database.models.Station; +import telraam.logic.lapper.Lapper; +import telraam.logic.positioner.Positioner; +import telraam.station.Fetcher; +import telraam.station.models.RonnyDetection; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.*; +import java.sql.Timestamp; +import java.util.*; +import java.util.logging.Logger; + +public class WebsocketFetcher implements Fetcher { + private final Set lappers; + private final Set positioners; + private Station station; + + private final BatonDAO batonDAO; + private final DetectionDAO detectionDAO; + private final StationDAO stationDAO; + + private final HttpClient client = HttpClient.newHttpClient(); + private final Logger logger = Logger.getLogger(WebsocketFetcher.class.getName()); + + public WebsocketFetcher(Jdbi database, Station station, Set lappers, Set positioners) { + this.batonDAO = database.onDemand(BatonDAO.class); + this.detectionDAO = database.onDemand(DetectionDAO.class); + this.stationDAO = database.onDemand(StationDAO.class); + this.lappers = lappers; + this.positioners = positioners; + + this.station = station; + } + + public void fetch() { + logger.info("Running Fetcher for station(" + this.station.getId() + ")"); + ObjectMapper mapper = new ObjectMapper(); + + //Update the station to account for possible changes in the database + this.stationDAO.getById(station.getId()).ifPresentOrElse( + station -> this.station = station, + () -> this.logger.severe("Can't update station from database.") + ); + + //Get last detection id + int lastDetectionId = 0; + Optional lastDetection = detectionDAO.latestDetectionByStationId(this.station.getId()); + if (lastDetection.isPresent()) { + lastDetectionId = lastDetection.get().getRemoteId(); + } + + InitWSMessage wsMessage = new InitWSMessage(lastDetectionId); + String wsMessageEncoded; + try { + wsMessageEncoded = mapper.writeValueAsString(wsMessage); + } catch (JsonProcessingException e) { + logger.severe(e.getMessage()); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException ex) { + logger.severe(ex.getMessage()); + } + this.fetch(); + return; + } + + //Create URL + URI url; + try { + url = new URI(station.getUrl()); + } catch (URISyntaxException ex) { + this.logger.severe(ex.getMessage()); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + this.fetch(); + return; + } + + WebsocketClient websocketClient = new WebsocketClient(url); + websocketClient.addOnOpenHandler(() -> { + websocketClient.sendMessage(wsMessageEncoded); + }); + websocketClient.addOnCloseHandler(() -> { + this.logger.severe(String.format("Websocket for station %s got closed", station.getName())); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + this.fetch(); + }); + websocketClient.addMessageHandler((String msg) -> { + //Insert detections + List new_detections = new ArrayList<>(); + List detection_mac_addresses = new ArrayList<>(); + + try { + List detections = Arrays.asList(mapper.readValue(msg, RonnyDetection[].class)); + for (RonnyDetection detection : detections) { + new_detections.add(new Detection( + 0, + station.getId(), + detection.rssi, + detection.battery, + detection.uptimeMs, + detection.id, + new Timestamp((long) (detection.detectionTimestamp * 1000)), + new Timestamp(System.currentTimeMillis()) + )); + detection_mac_addresses.add(detection.mac.toUpperCase()); + } + if (!new_detections.isEmpty()) { + List db_detections = detectionDAO.insertAllWithoutBaton(new_detections, detection_mac_addresses); + for(int i = 0; i < new_detections.size(); i++) { + Detection detection = new_detections.get(i); + Detection db_detection = db_detections.get(i); + + detection.setBatonId(db_detection.getBatonId()); + detection.setId(db_detection.getId()); + + lappers.forEach((lapper) -> lapper.handle(detection)); + positioners.forEach(positioner -> positioner.handle(detection)); + } + } + + logger.finer("Fetched " + detections.size() + " detections from " + station.getName() + ", Saved " + new_detections.size()); + } catch (JsonProcessingException e) { + logger.severe(e.getMessage()); + } + }); + + try { + websocketClient.listen(); + } catch (RuntimeException ex) { + this.logger.severe(ex.getMessage()); + try { + Thread.sleep(Fetcher.ERROR_TIMEOUT_MS); + } catch (InterruptedException e) { + logger.severe(e.getMessage()); + } + this.fetch(); + return; + } + } + + @AllArgsConstructor + private static class InitWSMessage { + public int lastId; + } +} diff --git a/src/main/java/telraam/util/AcceptedLapsUtil.java b/src/main/java/telraam/util/AcceptedLapsUtil.java index f6cac0a..c0f747b 100644 --- a/src/main/java/telraam/util/AcceptedLapsUtil.java +++ b/src/main/java/telraam/util/AcceptedLapsUtil.java @@ -23,8 +23,8 @@ public AcceptedLapsUtil(LapDAO lapDAO, LapSourceSwitchoverDAO lapSourceSwitchove public static void createInstance(Jdbi jdbi) { instance = new AcceptedLapsUtil( - jdbi.onDemand(LapDAO.class), - jdbi.onDemand(LapSourceSwitchoverDAO.class) + jdbi.onDemand(LapDAO.class), + jdbi.onDemand(LapSourceSwitchoverDAO.class) ); } diff --git a/src/main/java/telraam/websocket/WebSocketConnection.java b/src/main/java/telraam/websocket/WebSocketConnection.java new file mode 100644 index 0000000..191fea0 --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketConnection.java @@ -0,0 +1,41 @@ +package telraam.websocket; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; + +public class WebSocketConnection extends WebSocketAdapter { + private static final Logger logger = Logger.getLogger(WebSocketConnection.class.getName()); + + @Override + public void onWebSocketConnect(Session session) { + super.onWebSocketConnect(session); + WebSocketMessageSingleton.getInstance().registerConnection(this); + logger.info("Instance with remote \"%s\" connected".formatted(getRemote().getRemoteAddress())); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + super.onWebSocketClose(statusCode, reason); + WebSocketMessageSingleton.getInstance().unregisterConnection(this); + logger.info("Instance with remote \"%s\" closed: [%s] %s".formatted(getRemote().getRemoteAddress(), statusCode, reason)); + } + + @Override + public void onWebSocketError(Throwable cause) { + super.onWebSocketError(cause); + logger.severe("WebSocket error in instance with remote \"%s\": %s".formatted(getRemote().getRemoteAddress(), cause)); + } + + public void send(String s) { + try { + getRemote().sendString(s); + } catch (IOException e) { + logger.severe("Sending \"%s\" through instance with remote \"%s\" failed with %s".formatted(s, getRemote().getRemoteAddress(), e)); + return; + } + logger.finest("Sent \"%s\" through instance with remote \"%s\"".formatted(s, getRemote().getRemoteAddress())); + } +} diff --git a/src/main/java/telraam/websocket/WebSocketMessage.java b/src/main/java/telraam/websocket/WebSocketMessage.java new file mode 100644 index 0000000..b280ec2 --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketMessage.java @@ -0,0 +1,11 @@ +package telraam.websocket; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter @Setter @NoArgsConstructor +public class WebSocketMessage { + private String topic; + private T data; +} diff --git a/src/main/java/telraam/websocket/WebSocketMessageSingleton.java b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java new file mode 100644 index 0000000..e56b683 --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java @@ -0,0 +1,48 @@ +package telraam.websocket; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +@NoArgsConstructor +public class WebSocketMessageSingleton { + private static final Logger logger = Logger.getLogger(WebSocketMessageSingleton.class.getName()); + + @Getter + private static final WebSocketMessageSingleton instance = new WebSocketMessageSingleton(); + private final Set registeredConnections = new HashSet<>(); + private final ObjectMapper mapper = new ObjectMapper(); + + public void registerConnection(WebSocketConnection conn) { + boolean modified = registeredConnections.add(conn); + if (modified) { + logger.info("Registered WebSocketConnection %s".formatted(conn)); + } + } + + public void unregisterConnection(WebSocketConnection conn) { + boolean modified = registeredConnections.remove(conn); + if (modified) { + logger.info("Unregistered WebSocketConnection %s".formatted(conn)); + } + } + + public void sendToAll(String s) { + logger.finest("Sending \"%s\" to all registered WebSocketConnection instances".formatted(s)); + registeredConnections.forEach(conn -> conn.send(s)); + } + + public void sendToAll(WebSocketMessage message) { + try { + String json = mapper.writeValueAsString(message); + this.sendToAll(json); + } catch (JsonProcessingException e) { + logger.severe("Json conversion error for \"%s\"".formatted(message.toString())); + } + } +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index f11cd06..46bfca7 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -1 +1,8 @@ -ZeusWPI - Telraam \ No newline at end of file + + + _____ __ ______ ___ _____ _ +|__ /___ _ _ ___ \ \ / / _ \_ _| |_ _|__| |_ __ __ _ __ _ _ __ ___ + / // _ \ | | / __| \ \ /\ / /| |_) | | _____ | |/ _ \ | '__/ _` |/ _` | '_ ` _ \ + / /| __/ |_| \__ \ \ V V / | __/| | |_____| | | __/ | | | (_| | (_| | | | | | | +/____\___|\__,_|___/ \_/\_/ |_| |___| |_|\___|_|_| \__,_|\__,_|_| |_| |_| + diff --git a/src/main/resources/db/migration/V15__remove_null_constraint_on_newbatonid.sql b/src/main/resources/db/migration/V15__remove_null_constraint_on_newbatonid.sql new file mode 100644 index 0000000..0f7ae17 --- /dev/null +++ b/src/main/resources/db/migration/V15__remove_null_constraint_on_newbatonid.sql @@ -0,0 +1 @@ +alter table batonswitchover alter column newbatonid drop not null; \ No newline at end of file diff --git a/src/main/resources/db/migration/V16__Add_team_jacket_nr.sql b/src/main/resources/db/migration/V16__Add_team_jacket_nr.sql new file mode 100644 index 0000000..9b261db --- /dev/null +++ b/src/main/resources/db/migration/V16__Add_team_jacket_nr.sql @@ -0,0 +1 @@ +alter table team add jacket_nr varchar(255) default 'INVALID' not null; \ No newline at end of file diff --git a/src/main/resources/db/migration/V17__Add_position_source.sql b/src/main/resources/db/migration/V17__Add_position_source.sql new file mode 100644 index 0000000..fa8c759 --- /dev/null +++ b/src/main/resources/db/migration/V17__Add_position_source.sql @@ -0,0 +1,6 @@ +create table position_source +( + id serial not null + constraint position_source_pk primary key, + name varchar(255) not null unique +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V18__Remove_baton_id_from_teams.sql b/src/main/resources/db/migration/V18__Remove_baton_id_from_teams.sql new file mode 100644 index 0000000..0408a2f --- /dev/null +++ b/src/main/resources/db/migration/V18__Remove_baton_id_from_teams.sql @@ -0,0 +1,9 @@ +ALTER TABLE team DROP COLUMN baton_id; + +CREATE VIEW team_baton_ids (team_id, baton_id) +AS +SELECT bs.teamid, newbatonid +FROM batonswitchover bs + INNER JOIN (SELECT teamid, MAX(timestamp) AS max_timestamp + FROM batonswitchover + GROUP BY teamid) latest ON bs.teamid = latest.teamid AND bs.timestamp = latest.max_timestamp diff --git a/src/main/resources/telraam/devConfig.yml b/src/main/resources/telraam/devConfig.yml index f3b5a8f..db4adc8 100644 --- a/src/main/resources/telraam/devConfig.yml +++ b/src/main/resources/telraam/devConfig.yml @@ -46,6 +46,9 @@ database: # the minimum amount of time an connection must sit idle in the pool before it is eligible for eviction minIdleTime: 1 minute +# Distance between the start and the last station +finish_offset: 20 + # Logging settings. logging: diff --git a/src/test/java/telraam/DatabaseTest.java b/src/test/java/telraam/DatabaseTest.java index f490379..ac1f4f8 100644 --- a/src/test/java/telraam/DatabaseTest.java +++ b/src/test/java/telraam/DatabaseTest.java @@ -1,8 +1,8 @@ package telraam; +import io.dropwizard.core.setup.Environment; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.ManagedDataSource; -import io.dropwizard.setup.Environment; import io.dropwizard.testing.ResourceHelpers; import io.dropwizard.testing.junit5.DropwizardAppExtension; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; @@ -34,6 +34,7 @@ public static void initialize() { flyway = Flyway.configure() .dataSource(ds) .schemas() + .cleanDisabled(false) .load(); jdbi = ((App) APP_EXTENSION.getApplication()).getDatabase(); } diff --git a/src/test/java/telraam/api/BatonResourceTest.java b/src/test/java/telraam/api/BatonResourceTest.java index 8aca073..95a085c 100644 --- a/src/test/java/telraam/api/BatonResourceTest.java +++ b/src/test/java/telraam/api/BatonResourceTest.java @@ -1,5 +1,6 @@ package telraam.api; +import jakarta.ws.rs.WebApplicationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import telraam.DatabaseTest; @@ -9,7 +10,6 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; -import javax.ws.rs.WebApplicationException; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/telraam/database/daos/StationDAOTest.java b/src/test/java/telraam/database/daos/StationDAOTest.java index 64dceca..2bd64bc 100644 --- a/src/test/java/telraam/database/daos/StationDAOTest.java +++ b/src/test/java/telraam/database/daos/StationDAOTest.java @@ -34,7 +34,7 @@ void createStation() { Station station = stationOptional.get(); assertEquals("teststation", station.getName()); assertEquals(1d, station.getDistanceFromStart()); - assertEquals(false, station.getIsBroken()); + assertEquals(false, station.getBroken()); assertEquals("localhost:8000", station.getUrl()); } @@ -83,6 +83,7 @@ void testUpdateDoesUpdate() { testStation.setId(testid); testStation.setName("postUpdate"); testStation.setDistanceFromStart(2d); + testStation.setCoordY(0.69); testStation.setBroken(true); testStation.setUrl("localhost:8001"); int updatedRows = stationDAO.update(testid, testStation); @@ -92,7 +93,8 @@ void testUpdateDoesUpdate() { assertFalse(dbStation.isEmpty()); assertEquals("postUpdate", dbStation.get().getName()); assertEquals(2d, dbStation.get().getDistanceFromStart()); - assertEquals(true, dbStation.get().getIsBroken()); + assertEquals(true, dbStation.get().getBroken()); + assertEquals(0.69, dbStation.get().getCoordY()); assertEquals("localhost:8001", dbStation.get().getUrl()); } diff --git a/src/test/java/telraam/database/daos/TeamDAOTest.java b/src/test/java/telraam/database/daos/TeamDAOTest.java index 775efa9..16c6e0d 100644 --- a/src/test/java/telraam/database/daos/TeamDAOTest.java +++ b/src/test/java/telraam/database/daos/TeamDAOTest.java @@ -5,10 +5,13 @@ import org.junit.jupiter.api.Test; import telraam.DatabaseTest; import telraam.database.models.Baton; +import telraam.database.models.BatonSwitchover; import telraam.database.models.Team; import java.util.List; import java.util.Optional; +import java.sql.Timestamp; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.*; @@ -16,6 +19,7 @@ class TeamDAOTest extends DatabaseTest { private TeamDAO teamDAO; private BatonDAO batonDAO; + private BatonSwitchoverDAO batonSwitchoverDAO; @Override @BeforeEach @@ -23,6 +27,7 @@ public void setUp() throws Exception { super.setUp(); teamDAO = jdbi.onDemand(TeamDAO.class); batonDAO = jdbi.onDemand(BatonDAO.class); + batonSwitchoverDAO = jdbi.onDemand(BatonSwitchoverDAO.class); } @Test @@ -44,6 +49,10 @@ void testCreateTeamWithBaton() { Team testteam = new Team("testteam", batonId); int testId = teamDAO.insert(testteam); + LocalDateTime dateTime = LocalDateTime.now(); + BatonSwitchover switchover = new BatonSwitchover(testId, null, batonId, Timestamp.valueOf(dateTime)); + batonSwitchoverDAO.insert(switchover); + assertTrue(testId > 0); Optional teamOptional = teamDAO.getById(testId); assertFalse(teamOptional.isEmpty()); @@ -60,14 +69,6 @@ void testInsertFailsWhenNoName() { } - @Test - void testInsertFailsWhenInvalidBaton() { - Team testteam = new Team("testtteam", 1); - assertThrows(UnableToExecuteStatementException.class, - () -> teamDAO.insert(testteam)); - - } - @Test void testListTeamsEmpty() { List teams = teamDAO.getAll(); @@ -103,28 +104,14 @@ void testUpdateDoesUpdate() { int testid = teamDAO.insert(testTeam); testTeam.setId(testid); testTeam.setName("postupdate"); + testTeam.setJacketNr("10"); int updatedRows = teamDAO.update(testid, testTeam); assertEquals(1, updatedRows); Optional dbTeam = teamDAO.getById(testid); assertFalse(dbTeam.isEmpty()); assertEquals("postupdate", dbTeam.get().getName()); - } - - @Test - void testUpdateFailsWhenInvalidBaton() { - Baton testBaton = new Baton("testbaton", "mac1"); - int batonId = batonDAO.insert(testBaton); - Team testTeam = new Team("testteam", batonId); - int teamId = teamDAO.insert(testTeam); - // TODO: this is a little awkward, monitor - // if this happens often in the real code - // and find a better way - testTeam.setId(teamId); - - testTeam.setBatonId(batonId + 1); - assertThrows(UnableToExecuteStatementException.class, - () -> teamDAO.update(teamId, testTeam)); + assertEquals("10", dbTeam.get().getJacketNr()); } @Test diff --git a/src/test/java/telraam/logic/SimpleLapperTest.java b/src/test/java/telraam/logic/simple/SimpleLapperTest.java similarity index 94% rename from src/test/java/telraam/logic/SimpleLapperTest.java rename to src/test/java/telraam/logic/simple/SimpleLapperTest.java index 4214d13..c50e810 100644 --- a/src/test/java/telraam/logic/SimpleLapperTest.java +++ b/src/test/java/telraam/logic/simple/SimpleLapperTest.java @@ -1,4 +1,4 @@ -package telraam.logic; +package telraam.logic.simple; import org.jdbi.v3.core.Jdbi; import org.junit.jupiter.api.BeforeEach; @@ -8,6 +8,8 @@ import telraam.database.models.Detection; import telraam.database.models.Lap; import telraam.database.models.LapSource; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.simple.SimpleLapper; import java.sql.Timestamp; import java.util.Optional;