From b08291edbef29638bd0c25e97704825a7fedad70 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Sun, 5 Apr 2020 21:11:50 +0200 Subject: [PATCH] some refactoring --- .gitignore | 2 +- README.md | 1 + build.gradle | 41 ++ gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 183 +++++++ gradlew.bat | 103 ++++ settings.gradle | 4 + src/main/java/de/wwwu/awolf/App.java | 61 +++ .../java/de/wwwu/awolf/model/Interval.java | 79 +++ src/main/java/de/wwwu/awolf/model/Line.java | 297 +++++++++++ .../java/de/wwwu/awolf/model/LineModel.java | 145 ++++++ src/main/java/de/wwwu/awolf/model/Pair.java | 54 ++ src/main/java/de/wwwu/awolf/model/Point.java | 119 +++++ .../model/communication/AlgorithmData.java | 37 ++ .../wwwu/awolf/model/communication/Data.java | 8 + .../model/communication/EvaluationData.java | 77 +++ .../model/communication/GeneratorData.java | 43 ++ .../awolf/model/communication/ImportData.java | 34 ++ .../model/communication/SubscriberType.java | 13 + .../awolf/model/communication/TypeData.java | 16 + .../model/evaluation/ComparisonResult.java | 24 + .../awolf/presenter/AbstractPresenter.java | 168 +++++++ .../de/wwwu/awolf/presenter/Presenter.java | 134 +++++ .../awolf/presenter/algorithms/Algorithm.java | 49 ++ .../algorithms/AlgorithmHandler.java | 97 ++++ .../LeastMedianOfSquaresEstimator.java | 467 ++++++++++++++++++ .../advanced/RepeatedMedianEstimator.java | 356 +++++++++++++ .../advanced/TheilSenEstimator.java | 248 ++++++++++ .../NaivLeastMedianOfSquaresEstimator.java | 143 ++++++ .../naiv/NaivRepeatedMedianEstimator.java | 165 +++++++ .../naiv/NaivTheilSenEstimator.java | 114 +++++ .../awolf/presenter/data/DataHandler.java | 89 ++++ .../generator/CircleDatasetGenerator.java | 63 +++ .../data/generator/CloudDatasetGenerator.java | 53 ++ .../data/generator/DatasetGenerator.java | 67 +++ .../data/generator/LineDatasetGenerator.java | 35 ++ .../awolf/presenter/data/io/DataExporter.java | 61 +++ .../awolf/presenter/data/io/DataImporter.java | 88 ++++ .../evaluation/AlgorithmComparison.java | 48 ++ .../evaluation/EvaluatationHandler.java | 282 +++++++++++ .../measures/PercentageErrorBasedMeasure.java | 111 +++++ .../measures/ScaleDependentMeasure.java | 90 ++++ .../measures/ScaledErrorBasedMeasure.java | 108 ++++ .../presenter/util/BinomialCoeffizient.java | 33 ++ .../YOrderLineComparatorBegin.java | 19 + .../Comparators/YOrderLineComparatorEnd.java | 19 + .../presenter/util/FastElementSelector.java | 95 ++++ .../presenter/util/IntersectionComputer.java | 173 +++++++ .../de/wwwu/awolf/presenter/util/Logging.java | 50 ++ .../awolf/presenter/util/RandomSampler.java | 44 ++ .../de/wwwu/awolf/view/ViewController.java | 213 ++++++++ .../controller/AlgorithmTabController.java | 119 +++++ .../view/services/ButtonClickService.java | 39 ++ .../wwwu/awolf/view/services/DataService.java | 51 ++ .../view/services/GuiRegisterService.java | 28 ++ ....wwwu.awolf.presenter.algorithms.Algorithm | 6 + src/main/resources/Thumbs.db | Bin 0 -> 34304 bytes src/main/resources/icons/export.png | Bin 0 -> 1328 bytes src/main/resources/icons/frame-2.png | Bin 0 -> 764 bytes src/main/resources/icons/frame.png | Bin 0 -> 1080 bytes src/main/resources/icons/generate.png | Bin 0 -> 5087 bytes src/main/resources/icons/import.png | Bin 0 -> 1042 bytes src/main/resources/icons/plot.png | Bin 0 -> 1735 bytes src/main/resources/icons/start.png | Bin 0 -> 2879 bytes src/main/resources/icons/wwu.png | Bin 0 -> 18308 bytes src/main/resources/log4j.properties | 14 + src/main/resources/style/style.css | 340 +++++++++++++ src/main/resources/views/AlgorithmTab.fxml | 64 +++ src/main/resources/views/MainFrame.fxml | 22 + .../LeastMedianOfSquaresEstimatorTest.java | 37 ++ .../advanced/TheilSenEstimatorTest.java | 31 ++ .../util/FastElementSelectorTest.java | 33 ++ .../util/IntersectionCounterTest.java | 40 ++ 75 files changed, 5853 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/de/wwwu/awolf/App.java create mode 100644 src/main/java/de/wwwu/awolf/model/Interval.java create mode 100644 src/main/java/de/wwwu/awolf/model/Line.java create mode 100644 src/main/java/de/wwwu/awolf/model/LineModel.java create mode 100644 src/main/java/de/wwwu/awolf/model/Pair.java create mode 100644 src/main/java/de/wwwu/awolf/model/Point.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/AlgorithmData.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/Data.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/EvaluationData.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/GeneratorData.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/ImportData.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/SubscriberType.java create mode 100644 src/main/java/de/wwwu/awolf/model/communication/TypeData.java create mode 100644 src/main/java/de/wwwu/awolf/model/evaluation/ComparisonResult.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/AbstractPresenter.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/Presenter.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/Algorithm.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/AlgorithmHandler.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/RepeatedMedianEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivLeastMedianOfSquaresEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivRepeatedMedianEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivTheilSenEstimator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/DataHandler.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/generator/CircleDatasetGenerator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/generator/CloudDatasetGenerator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/generator/DatasetGenerator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/generator/LineDatasetGenerator.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/io/DataExporter.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/data/io/DataImporter.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/evaluation/AlgorithmComparison.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/evaluation/EvaluatationHandler.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/evaluation/measures/PercentageErrorBasedMeasure.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaleDependentMeasure.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaledErrorBasedMeasure.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/BinomialCoeffizient.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorBegin.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorEnd.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/FastElementSelector.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/IntersectionComputer.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/Logging.java create mode 100644 src/main/java/de/wwwu/awolf/presenter/util/RandomSampler.java create mode 100644 src/main/java/de/wwwu/awolf/view/ViewController.java create mode 100644 src/main/java/de/wwwu/awolf/view/controller/AlgorithmTabController.java create mode 100644 src/main/java/de/wwwu/awolf/view/services/ButtonClickService.java create mode 100644 src/main/java/de/wwwu/awolf/view/services/DataService.java create mode 100644 src/main/java/de/wwwu/awolf/view/services/GuiRegisterService.java create mode 100644 src/main/resources/META-INF/services/de.wwwu.awolf.presenter.algorithms.Algorithm create mode 100644 src/main/resources/Thumbs.db create mode 100644 src/main/resources/icons/export.png create mode 100644 src/main/resources/icons/frame-2.png create mode 100644 src/main/resources/icons/frame.png create mode 100644 src/main/resources/icons/generate.png create mode 100644 src/main/resources/icons/import.png create mode 100644 src/main/resources/icons/plot.png create mode 100644 src/main/resources/icons/start.png create mode 100644 src/main/resources/icons/wwu.png create mode 100644 src/main/resources/log4j.properties create mode 100644 src/main/resources/style/style.css create mode 100644 src/main/resources/views/AlgorithmTab.fxml create mode 100644 src/main/resources/views/MainFrame.fxml create mode 100644 src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimatorTest.java create mode 100644 src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimatorTest.java create mode 100644 src/test/java/de/wwwu/awolf/presenter/util/FastElementSelectorTest.java create mode 100644 src/test/java/de/wwwu/awolf/presenter/util/IntersectionCounterTest.java diff --git a/.gitignore b/.gitignore index a8f28db..8a30089 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ log/ target/ # Ignore Gradle project-specific cache directory -.gradle +.gradle/ # Ignore Gradle build output directory build diff --git a/README.md b/README.md new file mode 100644 index 0000000..0647d92 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# README # diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..490ceb2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id 'java' + id "org.sonarqube" version "2.7" +} + +apply plugin: 'org.sonarqube' + +repositories { + mavenLocal() + mavenCentral() + maven { + url = uri('http://repo.maven.apache.org/maven2') + } +} + +dependencies { + implementation 'junit:junit:4.12' + implementation 'commons-io:commons-io:2.5' + implementation 'log4j:log4j:1.2.17' + implementation 'org.apache.logging.log4j:log4j-core:2.13.1' + implementation 'com.opencsv:opencsv:5.1' + implementation 'com.google.guava:guava:28.2-jre' + implementation 'org.powermock:powermock-core:2.0.6' + compile group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1' +} + +group = 'de.wwu.awolf' +version = '1.0.0' +sourceCompatibility = '11' + +sonarqube { + properties { + property "sonar.projectName", "${rootProject.name}" + property "sonar.projectKey", "${project.group}:${rootProject.name}" + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..409b361 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +systemProp.sonar.host.url=http://192.168.0.158:9000 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..490fda8577df6c95960ba7077c43220e5bb2c0d9 GIT binary patch literal 58694 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0e8b331 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Mar 25 19:04:29 CET 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..62bd9b9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0c2a461 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +/* + * This file was generated by the Gradle 'init' task. + */ +rootProject.name = 'linreg-tool' diff --git a/src/main/java/de/wwwu/awolf/App.java b/src/main/java/de/wwwu/awolf/App.java new file mode 100644 index 0000000..18f8fcc --- /dev/null +++ b/src/main/java/de/wwwu/awolf/App.java @@ -0,0 +1,61 @@ +package de.wwwu.awolf; + +import de.wwwu.awolf.presenter.util.Logging; +import de.wwwu.awolf.view.ViewController; +import de.wwwu.awolf.view.services.GuiRegisterService; +import java.io.IOException; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class App extends Application { + + private static final String TITLE = "Algorithmen zur Berechnung von Ausgleichsgeraden"; + + /** + * Maim Methode + * + * @param argv + */ + public static void main(String[] argv) { + Logging.logDebug("Start ...."); + Application.launch(App.class, argv); + } + + @Override + public void start(Stage primaryStage) throws Exception { + Platform.runLater(() -> { + + try { + FXMLLoader fxmlLoader = new FXMLLoader( + ViewController.class.getResource("/views/MainFrame.fxml")); + Parent pane = fxmlLoader.load(); + //register at presenter + new GuiRegisterService(fxmlLoader.getController()).start(); + primaryStage.setTitle(TITLE); + Scene scene = new Scene(pane, 1200, 900); + scene.getStylesheets() + .add(ViewController.class.getResource("/style/style.css") + .toExternalForm()); + primaryStage.setScene(scene); + primaryStage.setOnCloseRequest(e -> { + Platform.exit(); + System.exit(0); + }); + primaryStage.show(); + } catch (IOException e) { + Logging.logError("Error reading FXML file. ", e); + } + }); + } +} diff --git a/src/main/java/de/wwwu/awolf/model/Interval.java b/src/main/java/de/wwwu/awolf/model/Interval.java new file mode 100644 index 0000000..d8ab46e --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/Interval.java @@ -0,0 +1,79 @@ +package de.wwwu.awolf.model; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 16.06.2017. + */ +public class Interval { + + private double upper; + private double lower; + private Boolean activity; + + /** + * Konstruktor + * + * @param lower untere Schranke + * @param upper obere Schranke + */ + public Interval(double lower, double upper) { + this.upper = upper; + this.lower = lower; + this.activity = true; + } + + /** + * @return true falls das Intervall aktiv ist + */ + public Boolean getActivity() { + return activity; + } + + /** + * @param isActive true falls das Intervall als aktiv gesetzt werden soll + */ + public void setActivity(Boolean isActive) { + this.activity = isActive; + } + + /** + * @return obere Schranke des Intervalls + */ + public double getUpper() { + return upper; + } + + /** + * @param upper obere Schranke des Intervalls + */ + public void setUpper(double upper) { + this.upper = upper; + } + + /** + * @return untere Schranke des Intervalls + */ + public double getLower() { + return lower; + } + + /** + * @param lower untere Schranke des Intervalls + */ + public void setLower(double lower) { + this.lower = lower; + } + + /** + * Berechnet die Distanz zwischen der oberen und unteren Schranke + * + * @return Distanz + */ + public Double getDistance() { + return Math.abs(this.upper - this.lower); + } + +} \ No newline at end of file diff --git a/src/main/java/de/wwwu/awolf/model/Line.java b/src/main/java/de/wwwu/awolf/model/Line.java new file mode 100644 index 0000000..82dd234 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/Line.java @@ -0,0 +1,297 @@ +package de.wwwu.awolf.model; + +import java.util.Objects; +import org.apache.commons.math3.util.Precision; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 12.06.2017. + */ +public class Line implements Comparable { + + private static final double EPSILON = 0.00001; + private static final double MIN = 9999d; + private static final double MAX = -9999d; + + + private Double m; + private Double b; + + private Double x1; + private Double x2; + private Double y1; + private Double y2; + + private String id; + + /** + * Konstruktor + * + * @param m Steigung + * @param b y-Achsenabschnitt + * @param id id + */ + public Line(double m, double b, String id) { + this.m = m; + this.b = b; + + this.x1 = MAX; + this.y1 = (MAX * m) + b; + this.x2 = MIN * 0.5; + this.y2 = ((MIN * 0.5) * m) + b; + this.id = id; + } + + /** + * Konstruktor + * + * @param m Steigung + * @param b y-Achsenabschnitt + */ + public Line(double m, double b) { + this.m = m; + this.b = b; + + this.x1 = calculateX1(MAX); + this.y1 = calculateY1(MAX); + this.x2 = calculateX2(MIN * 0.5); + this.y2 = calculateY2(MIN * 0.5); + } + + /** + * Konstruktor + * + * @param x1 x-Koordiante des Startpunkts + * @param x2 x-Koordinate des Endpunkts + * @param y1 y-Koordinate des Startpunkts + * @param y2 y-Koordinate des Endpunkts + */ + public Line(double x1, double x2, double y1, double y2) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + + this.m = (y2 - y1) / (x2 - x1); + this.b = y2 - (x2 * m); + + } + + public Double calculateX1(Double min) { + return min; + } + + public Double calculateY1(Double min) { + return (min * (m * -1)) + b; + } + + public Double calculateX2(Double max) { + return max; + } + + public Double calculateY2(Double max) { + return (max * (m * -1)) + b; + } + + /** + * @return Steigung der Gerade + */ + public Double getM() { + return m; + } + + /** + * @param m Steigung der Gerade + */ + public void setM(double m) { + this.m = m; + } + + /** + * @return y-Achsenabschnitt der Gerade + */ + public Double getB() { + return b; + } + + /** + * @param b y-Achsenabschnitt der Gerade + */ + public void setB(double b) { + this.b = b; + } + + /** + * @return id der Gerade + */ + public String getId() { + return id; + } + + /** + * @param id id der Gerade + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return x-Koordiante des Startpunkts + */ + public Double getX1() { + return x1; + } + + /** + * @return x-Koordiante des Endpunkts + */ + public Double getX2() { + return x2; + } + + /** + * @return y-Koordiante des Startpunkts + */ + public Double getY1() { + return y1; + } + + /** + * @return y-Koordiante des Endpunkts + */ + public Double getY2() { + return y2; + } + + /** + * Setzt die Koordianten des Segments. Aus dem Segment wird eine Gerade berechnet. + * + * @param x1 x-Koordiante des Startpunkts + * @param y1 y-Koordiante des Endpunkts + * @param x2 x-Koordinate des Startpunkts + * @param y2 y-Koordinte des Endpunkts + */ + public void setEndPoints(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + this.m = (y2 - y1) / (x2 - x1); + this.b = y2 - (x2 * m); + } + + /** + * Vergleich einzelner Geradern + * + * @param obj zu vergleichende Gerade + * @return true falls die Geraden Gleich sind + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Line) { + Line other = (Line) obj; + return Precision.equals(other.getM(), this.getM(), EPSILON) && Precision + .equals(other.getB(), this.getB(), EPSILON); + } else { + return super.equals(obj); + } + } + + @Override + public int hashCode() { + return Objects.hash(Precision.round(m, 5), Precision.round(b, 5)); + } + + @Override + public String toString() { + return "Line m: " + this.getM() + ", b: " + this.getB(); + } + + + public Point intersect(Line line) { + double x = (line.b - this.b) / (this.m - line.m); + double y = this.m * x + this.b; + + return new Point(x, y); + } + + // Given three colinear points p, q, r, the function checks if +// point q lies on line segment 'pr' + public boolean onSegment(Point p, Point q, Point r) { + return q.getX() <= Math.max(p.getX(), r.getX()) && q.getX() >= Math + .min(p.getX(), r.getX()) && + q.getY() <= Math.max(p.getY(), r.getY()) && q.getY() >= Math + .min(p.getY(), r.getY()); + } + + // To find orientation of ordered triplet (p, q, r). + // The function returns following values + // 0 --> p, q and r are colinear + // 1 --> Clockwise + // 2 --> Counterclockwise + public int orientation(Point p, Point q, Point r) { + // See https://www.geeksforgeeks.org/orientation-3-ordered-points/ + // for details of below formula. + double val = (q.getY() - p.getY()) * (r.getX() - q.getX()) - + (q.getX() - p.getX()) * (r.getY() - q.getY()); + + if (val == 0) { + return 0; // colinear + } + + return (val > 0) ? 1 : 2; // clock or counterclock wise + } + + // The main function that returns true if line segment 'p1q1' + // and 'p2q2' intersect. + // Line A: y = mx + b --> + public boolean doIntersect(Line line, double lower, double upper) { + //this + Point p1 = new Point(calculateX1(lower), calculateY1(lower)); + Point q1 = new Point(calculateX2(upper), calculateY2(upper)); + Point p2 = new Point(line.calculateX1(lower), line.calculateY1(lower)); + Point q2 = new Point(line.calculateX2(upper), line.calculateY2(upper)); + // Find the four orientations needed for general and + // special cases + int o1 = orientation(p1, q1, p2); + int o2 = orientation(p1, q1, q2); + int o3 = orientation(p2, q2, p1); + int o4 = orientation(p2, q2, q1); + + // General case + if (o1 != o2 && o3 != o4) { + return true; + } + + // Special Cases + // p1, q1 and p2 are colinear and p2 lies on segment p1q1 + if (o1 == 0 && onSegment(p1, p2, q1)) { + return true; + } + + // p1, q1 and q2 are colinear and q2 lies on segment p1q1 + if (o2 == 0 && onSegment(p1, q2, q1)) { + return true; + } + + // p2, q2 and p1 are colinear and p1 lies on segment p2q2 + if (o3 == 0 && onSegment(p2, p1, q2)) { + return true; + } + + // p2, q2 and q1 are colinear and q1 lies on segment p2q2 + return o4 == 0 && onSegment(p2, q1, q2);// Doesn't fall in any of the above cases + } + + @Override + public int compareTo(Line line) { + if (Precision.compareTo(this.getM(), line.getM(), EPSILON) == 0) { + return this.getB().compareTo(line.getB()); + } else { + return this.getM().compareTo(line.getM()); + } + } +} diff --git a/src/main/java/de/wwwu/awolf/model/LineModel.java b/src/main/java/de/wwwu/awolf/model/LineModel.java new file mode 100644 index 0000000..9da5c1c --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/LineModel.java @@ -0,0 +1,145 @@ +package de.wwwu.awolf.model; + +import java.util.HashSet; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class LineModel { + + private Double max; + private Double min; + + private Set lines; + private Double xMinimum; + private Double xMaximum; + private Double yMinimum; + private Double yMaximum; + private int size; + + /** + * Konstruktor + */ + public LineModel() { + lines = new HashSet<>(); + size = 0; + + xMinimum = Double.MAX_VALUE; + xMaximum = Double.MIN_VALUE; + yMinimum = Double.MAX_VALUE; + yMaximum = Double.MIN_VALUE; + + min = 0d; + max = 0d; + } + + + /** + * Fügt eine Gerade zu dem Modell hinzu + * + * @param line Gerade + */ + public void addLine(Line line) { + this.lines.add(line); + min = line.getM() <= min ? line.getM() : min; + max = line.getM() >= max ? line.getM() : max; + } + + public Double getMin() { + return min; + } + + public Double getMax() { + return max; + } + + /** + * @return Liste der Geraden + */ + public Set getLines() { + return lines; + } + + /** + * @param lines Liste der Geraden + */ + public void setLines(Set lines) { + lines.forEach(this::addLine); + this.size = lines.size(); + } + + /** + * @return Minimale x-Koordiante + */ + public Double getxMinimum() { + return xMinimum; + } + + /** + * @param xMinimum Minimale x-Koordiante + */ + public void setxMinimum(Double xMinimum) { + this.xMinimum = xMinimum; + } + + /** + * @return Maximale x-Koordiante + */ + public Double getxMaximum() { + return xMaximum; + } + + /** + * @param xMaximum Maximale x-Koordiante + */ + public void setxMaximum(Double xMaximum) { + this.xMaximum = xMaximum; + } + + /** + * @return Minimale y-Koordiante + */ + public Double getyMinimum() { + return yMinimum; + } + + /** + * @param yMinimum Minimale y-Koordiante + */ + public void setyMinimum(Double yMinimum) { + this.yMinimum = yMinimum; + } + + /** + * @return Maximale y-Koordiante + */ + public Double getyMaximum() { + return yMaximum; + } + + /** + * @param yMaximum Maximale y-Koordiante + */ + public void setyMaximum(Double yMaximum) { + this.yMaximum = yMaximum; + } + + /** + * Setzt die minimalen, maximalen x- und y-Koordinaten + */ + public void resetRanges() { + xMinimum = Double.MAX_VALUE; + xMaximum = Double.MIN_VALUE; + yMinimum = Double.MAX_VALUE; + yMaximum = Double.MIN_VALUE; + } + + public int getSize() { + return size; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/Pair.java b/src/main/java/de/wwwu/awolf/model/Pair.java new file mode 100644 index 0000000..50786c9 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/Pair.java @@ -0,0 +1,54 @@ +package de.wwwu.awolf.model; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 19.06.2017. + */ +public class Pair { + + private Integer p1; + private Integer p2; + + /** + * Konstruktor + * + * @param p1 erstes Element des Tupels + * @param p2 zweites Element des Tupels + */ + public Pair(Integer p1, Integer p2) { + this.p1 = p1; + this.p2 = p2; + } + + /** + * @return erstes Element des Tupels + */ + public Integer getP1() { + return p1; + } + + /** + * @param p1 erstes Element des Tupels + */ + public void setP1(Integer p1) { + this.p1 = p1; + } + + /** + * @return zweites Element des Tupels + */ + public Integer getP2() { + return p2; + } + + /** + * @param p2 zweites Element des Tupels + */ + public void setP2(Integer p2) { + this.p2 = p2; + } + +} diff --git a/src/main/java/de/wwwu/awolf/model/Point.java b/src/main/java/de/wwwu/awolf/model/Point.java new file mode 100644 index 0000000..40467b2 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/Point.java @@ -0,0 +1,119 @@ +package de.wwwu.awolf.model; + +import java.util.Objects; +import org.apache.commons.math3.util.Precision; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class Point implements Comparable { + + private static final double EPSILON = 0.00001; + + private Double x; + private Double y; + private String id; + + /** + * Konstruktor + * + * @param x x-Koordiante + * @param y y-Koordiante + */ + public Point(Double x, Double y) { + this.x = x; + this.y = y; + } + + /** + * Konstruktor + * + * @param x x-Koordiante + * @param y y-Koordiante + * @param id id des Punkts + */ + public Point(Double x, Double y, String id) { + this.x = x; + this.y = y; + this.id = id; + } + + /** + * @return x-Koordinate des Punkts + */ + public Double getX() { + return x; + } + + /** + * @param x x-Koordinate des Punkts + */ + public void setX(Double x) { + this.x = x; + } + + /** + * @return y-Koordinate des Punkts + */ + public Double getY() { + return y; + } + + /** + * @param y y-Koordinate des Punkts + */ + public void setY(Double y) { + this.y = y; + } + + @Override + public int compareTo(Point o) { + if (Precision.compareTo(this.getX(), o.getX(), EPSILON) == 0) { + return this.getY().compareTo(o.getY()); + } else { + return this.getX().compareTo(o.getX()); + } + } + + /** + * Vergleich zweier Punkte + * + * @param obj zu vergleichernder Punkt + * @return true falls die Punkte gleich sind + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Point) { + Point other = (Point) obj; + return Precision.equals(other.getX(), this.getX(), EPSILON) && Precision + .equals(other.getY(), this.getY(), EPSILON); + } else { + return super.equals(obj); + } + } + + @Override + public int hashCode() { + return Objects.hash(Precision.round(x, 5), Precision.round(y, 5)); + } + + /** + * @return id des Punkts + */ + public String getId() { + return id; + } + + /** + * @param id id des Punkts + */ + public void setId(String id) { + this.id = id; + } + + +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/AlgorithmData.java b/src/main/java/de/wwwu/awolf/model/communication/AlgorithmData.java new file mode 100644 index 0000000..18456dc --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/AlgorithmData.java @@ -0,0 +1,37 @@ +package de.wwwu.awolf.model.communication; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.algorithms.Algorithm; + +public class AlgorithmData implements Data { + + private SubscriberType type; + private Algorithm.Type algorithmType; + private Line lineData; + + public Algorithm.Type getAlgorithmType() { + return algorithmType; + } + + public void setAlgorithmType(Algorithm.Type algorithmType) { + this.algorithmType = algorithmType; + } + + @Override + public SubscriberType getType() { + return type; + } + + @Override + public void setType(SubscriberType type) { + this.type = type; + } + + public Line getLineData() { + return lineData; + } + + public void setLineData(Line lineData) { + this.lineData = lineData; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/Data.java b/src/main/java/de/wwwu/awolf/model/communication/Data.java new file mode 100644 index 0000000..1042caf --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/Data.java @@ -0,0 +1,8 @@ +package de.wwwu.awolf.model.communication; + +public interface Data { + + SubscriberType getType(); + + void setType(SubscriberType type); +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/EvaluationData.java b/src/main/java/de/wwwu/awolf/model/communication/EvaluationData.java new file mode 100644 index 0000000..c23bfda --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/EvaluationData.java @@ -0,0 +1,77 @@ +package de.wwwu.awolf.model.communication; + +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +public class EvaluationData implements Data { + + private SubscriberType type; + private List algorithmtypes; + private List oneColumnresult; + private Map> multipleColumnResult; + + private int rowsPerColumn; + private int column; + private List labels; + + public int getRowsPerColumn() { + return rowsPerColumn; + } + + public void setRowsPerColumn(int rowsPerColumn) { + this.rowsPerColumn = rowsPerColumn; + } + + public int getColumn() { + return column; + } + + public void setColumn(int col) { + this.column = col; + } + + public List getLabels() { + return labels; + } + + public void setLabels(List tableInput) { + this.labels = tableInput; + } + + public List getAlgorithmtypes() { + return algorithmtypes; + } + + public void setAlgorithmtypes(List algorithmtype) { + this.algorithmtypes = algorithmtype; + } + + public List getOneColumnresult() { + return oneColumnresult; + } + + public void setOneColumnresult(List oneColumnresult) { + this.oneColumnresult = oneColumnresult; + } + + public Map> getMultipleColumnResult() { + return multipleColumnResult; + } + + public void setMultipleColumnResult( + Map> multipleColumnResult) { + this.multipleColumnResult = multipleColumnResult; + } + + @Override + public SubscriberType getType() { + return type; + } + + @Override + public void setType(SubscriberType type) { + this.type = type; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/GeneratorData.java b/src/main/java/de/wwwu/awolf/model/communication/GeneratorData.java new file mode 100644 index 0000000..436a7c6 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/GeneratorData.java @@ -0,0 +1,43 @@ +package de.wwwu.awolf.model.communication; + +public class GeneratorData implements Data { + + private SubscriberType type; + private String message; + private double m; + private double b; + + public double getM() { + return m; + } + + public void setM(double m) { + this.m = m; + } + + public double getB() { + return b; + } + + public void setB(double b) { + this.b = b; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public SubscriberType getType() { + return type; + } + + @Override + public void setType(SubscriberType type) { + this.type = type; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/ImportData.java b/src/main/java/de/wwwu/awolf/model/communication/ImportData.java new file mode 100644 index 0000000..2da1b90 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/ImportData.java @@ -0,0 +1,34 @@ +package de.wwwu.awolf.model.communication; + +public class ImportData implements Data { + + private SubscriberType type; + private int numberOfLines; + private int current; + + @Override + public SubscriberType getType() { + return type; + } + + @Override + public void setType(SubscriberType type) { + this.type = type; + } + + public int getNumberOfLines() { + return numberOfLines; + } + + public void setNumberOfLines(int numberOfLines) { + this.numberOfLines = numberOfLines; + } + + public int getCurrent() { + return current; + } + + public void setCurrent(int current) { + this.current = current; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/SubscriberType.java b/src/main/java/de/wwwu/awolf/model/communication/SubscriberType.java new file mode 100644 index 0000000..31056e9 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/SubscriberType.java @@ -0,0 +1,13 @@ +package de.wwwu.awolf.model.communication; + +public enum SubscriberType { + + EVAL_D, + EVALUATION_TABLE_DATA, + EVAL_T, + LINES_RES, + LINES_RES_MULT, + ALGORITHM, + PICTURE, + GENERATOR +} diff --git a/src/main/java/de/wwwu/awolf/model/communication/TypeData.java b/src/main/java/de/wwwu/awolf/model/communication/TypeData.java new file mode 100644 index 0000000..9740ed5 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/communication/TypeData.java @@ -0,0 +1,16 @@ +package de.wwwu.awolf.model.communication; + +public class TypeData implements Data { + + private SubscriberType type; + + @Override + public SubscriberType getType() { + return type; + } + + @Override + public void setType(SubscriberType type) { + this.type = type; + } +} diff --git a/src/main/java/de/wwwu/awolf/model/evaluation/ComparisonResult.java b/src/main/java/de/wwwu/awolf/model/evaluation/ComparisonResult.java new file mode 100644 index 0000000..5872427 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/model/evaluation/ComparisonResult.java @@ -0,0 +1,24 @@ +package de.wwwu.awolf.model.evaluation; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import java.util.EnumMap; +import java.util.Map; + +public class ComparisonResult { + + private final Map resultMapping; + + public ComparisonResult() { + this.resultMapping = new EnumMap<>(Algorithm.Type.class); + } + + public void put(final Algorithm.Type type, final Line lineResult) { + this.resultMapping.put(type, lineResult); + } + + public Line get(final Algorithm.Type type) { + return this.resultMapping.get(type); + } + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/AbstractPresenter.java b/src/main/java/de/wwwu/awolf/presenter/AbstractPresenter.java new file mode 100644 index 0000000..4f6542a --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/AbstractPresenter.java @@ -0,0 +1,168 @@ +package de.wwwu.awolf.presenter; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.LineModel; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.algorithms.AlgorithmHandler; +import de.wwwu.awolf.presenter.data.DataHandler; +import de.wwwu.awolf.presenter.evaluation.EvaluatationHandler; +import de.wwwu.awolf.presenter.util.Logging; +import de.wwwu.awolf.view.ViewController; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 10.09.2017. + */ +public abstract class AbstractPresenter implements Flow.Subscriber { + + private final ExecutorService executor; + private LineModel model; + private ViewController view; + private EvaluatationHandler evaluatationHandler; + private DataHandler dataHandler; + private AlgorithmHandler algorithmHandler; + + + /** + * Konstruktor + */ + public AbstractPresenter() { + Logging.logDebug("Create instance of Presenter."); + this.executor = Executors.newCachedThreadPool(); + this.dataHandler = new DataHandler(this); + this.algorithmHandler = AlgorithmHandler.getInstance(); + //empty model + this.model = new LineModel(); + //init values null + this.view = null; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + Logging.logInfo("New Subscription: " + subscription.toString()); + subscription.request(15); + } + + + @Override + public void onNext(Data data) { + Logging.logDebug("Presenter received message. Type: " + data.getType()); + switch (data.getType()) { + case EVALUATION_TABLE_DATA: + evaluatedDatas(data); + break; + case ALGORITHM: + visualizeAlgorithm(data); + break; + default: + break; + } + } + + protected abstract void visualizeAlgorithm(Data data); + + protected abstract void evaluatedDatas(Data data); + + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onComplete() { + + } + + /** + * Execute an algorithm specified by a type + * + * @param type algorithm type + * @param lines set of lines + */ + public void executeAlgorithmByType(Algorithm.Type type, Set lines) { + this.algorithmHandler.runAlgorithmByType(type, lines); + } + + /** + * Execute an algorithm specified by a type + * + * @param type algorithm type + * @param lines set of lines + */ + public void executeAlgorithmByType(Algorithm.Type type, Set lines, + BooleanProperty guiFlag) { + this.algorithmHandler.runAlgorithmByType(type, lines, guiFlag); + } + + /** + * Execute an algorithm specified by a type (use the Lines from the LineModel) + * + * @param type algorithm type + */ + public Line executeAlgorithmByType(Algorithm.Type type, BooleanProperty guiFlag) { + if (getModel().getSize() == 0) { + Logging.logDebug("No lines in the Model. Nothing to calculate."); + throw new IllegalArgumentException(); + } else { + Logging.logDebug("AlgorithmHandler will start " + type.getLabel() + ", with " + getModel().getSize()); + return this.algorithmHandler.runAlgorithmByType(type, getModel().getLines(), guiFlag); + } + } + + /** + * @return das zu grunde legende Modell + */ + public LineModel getModel() { + return model; + } + + /** + * @return die zu grunde legende View + */ + public ViewController getView() { + return view; + } + + /** + * @param view die zu grunde legende View + */ + public void registerView(ViewController view) { + this.view = view; + Logging.logDebug("View has been set."); + + //customize gui + Platform.runLater(() -> this.view.initGui()); + } + + /** + * @return Evaluation + */ + EvaluatationHandler getEvaluatationHandler() { + return evaluatationHandler; + } + + DataHandler getDataHandler() { + return dataHandler; + } + + /** + * Returns the instance of the ExecutorService + * + * @return ExecutorService instance + */ + public ExecutorService getExecutor() { + return Objects.requireNonNullElseGet(executor, Executors::newCachedThreadPool); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/Presenter.java b/src/main/java/de/wwwu/awolf/presenter/Presenter.java new file mode 100644 index 0000000..9b89857 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/Presenter.java @@ -0,0 +1,134 @@ +package de.wwwu.awolf.presenter; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.EvaluationData; +import de.wwwu.awolf.presenter.data.DataHandler; +import de.wwwu.awolf.presenter.evaluation.EvaluatationHandler; +import de.wwwu.awolf.presenter.util.Logging; +import java.io.File; +import java.util.Set; +import javafx.application.Platform; +import javax.swing.SwingUtilities; + + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class Presenter extends AbstractPresenter { + + private static Presenter instance; + + private Presenter() { + super(); + } + + public static Presenter getInstance() { + if (instance == null) { + instance = new Presenter(); + } + + return instance; + } + + @Override + protected void visualizeAlgorithm(Data data) { + AlgorithmData algorithmData = (AlgorithmData) data; + + Platform.runLater(() -> getView().getAlgorithmTabControllers().get(algorithmData.getAlgorithmType()).updatePlot(getModel(), algorithmData.getLineData()) + ); + Logging.logInfo("Type: " + algorithmData.getType() + ". Result: " + algorithmData.getLineData().toString()); + } + + @Override + protected void evaluatedDatas(Data data) { + EvaluationData evaluationData = (EvaluationData) data; + SwingUtilities + .invokeLater( + () -> getView().appendEvalResults(evaluationData.getMultipleColumnResult())); + } + + /*************************************************************************************************************************** + * Hilfsmethoden + ***************************************************************************************************************************/ + + + /** + * Startet den Export des akteullen Datensatzes (nur der Geraden) + * + * @param file Datei in der die Informationen gespeichert werden sollen + */ + public void exportDataset(File file) { + getDataHandler().exportData(file, getModel().getLines()); + } + + /** + * Startet den Export des akteullen Datensatzes im Rahmen der Evaluation (nur der Geraden) + * + * @param file Datei in der die Informationen gespeichert werden sollen + */ + public void exportEvaluationDataset(File file) { + getDataHandler().exportData(file, getEvaluatationHandler().getData()); + Logging.logInfo("Export was successful"); + } + + /** + * Startet den Import des Datensatzes. + * + * @param file importierender Datensatz + */ + public Set importDataset(File file) { + Set data = getDataHandler().getData(file); + getModel().setLines(data); + //Berechnung der Schnittpunkte und vis. der Ergebnisse (anz. Geraden, anz. Schnittpunkte) + Logging.logInfo("Import was successful! "); + Platform.runLater(() -> getView().getAlgorithmTabControllers().values().forEach(e -> e.updatePlot(getModel(), null)) + ); + return data; + } + + /** + * Startet das generieren der Datensätze der Größe n + * + * @param n Größe des Datensatzes + * @param type Art der Datensatzes + */ + public Set generateDataset(int n, DataHandler.DataType type) { + Set data = getDataHandler().getData(type, n); + getModel().setLines(data); + Logging.logInfo("Generate was successful!"); + Platform.runLater(() -> getView().getAlgorithmTabControllers().values().forEach(e -> e.updatePlot(getModel(), null)) + ); + return data; + } + + /** + * Startet die Evaluation zu einen gegegbenen Typ mit den Informationen zu den Datensatz. Beispielsweise kann ein Alg. auf mit verschiedenen Datensätzen untersucht werden, oder mehrere Algorithmen + * auf einem gegebenen Datensatz. + * + * @param typ Typ der Evaluation + * @param n Größe des Datensatzes + * @param alg code für die auszuführenden Algorithmen (siehe EvaluationPanel.checkSelection() Method) + * @param datasetTyp Typ des Datensatzes (Geradem Punktwolke, Kreis und Gerade) + */ + public void startEvaluation(int typ, int n, int alg, DataHandler.DataType datasetTyp) { + getExecutor().submit(new EvaluatationHandler(typ, n, alg, datasetTyp)); + } + + /** + * Startet die Evaluation zu einen gegegbenen Datensatz, der importiert wird. Beispielsweise kann ein Alg. auf mit verschiedenen Datensätzen untersucht werden, oder mehrere Algorithmen auf einem + * gegebenen Datensatz. + * + * @param typ Typ der Evaluation + * @param alg code für die auszuführenden Algorithmen (siehe EvaluationPanel.checkSelection() Method) + * @param file Typ des Datensatzes (Geradem Punktwolke, Kreis und Gerade) + */ + public void startEvaluation(int typ, int alg, File file) { + getExecutor().submit(new EvaluatationHandler(typ, alg, file)); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/Algorithm.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/Algorithm.java new file mode 100644 index 0000000..176f29f --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/Algorithm.java @@ -0,0 +1,49 @@ +package de.wwwu.awolf.presenter.algorithms; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.presenter.AbstractPresenter; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public interface Algorithm extends Callable, Flow.Publisher { + + /** + * Startet die Berechnung des jeweiligen Algorithmus. + */ + @Override + Line call(); + + void setInput(final Set lines); + + Algorithm.Type getType(); + + void setPresenter(AbstractPresenter presenter); + + enum Type { + LMS("Least Median of Squares"), + RM("Repeated Median"), + TS("Theil-Sen"), + NAIVE_LMS("Brute Force (LMS)"), + NAIVE_RM("Brute Force (RM)"), + NAIVE_TS("Brute Force (TS)"); + + public final String label; + + Type(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/AlgorithmHandler.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/AlgorithmHandler.java new file mode 100644 index 0000000..3a542c8 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/AlgorithmHandler.java @@ -0,0 +1,97 @@ +package de.wwwu.awolf.presenter.algorithms; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.presenter.util.Logging; +import java.util.EnumMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import javafx.beans.property.BooleanProperty; +import javax.annotation.Nullable; + +public class AlgorithmHandler { + + private static AlgorithmHandler instance; + private Map algorithmMapping; + + + private AlgorithmHandler() { + //lookup + lookUp(); + } + + /** + * Singleton getter + * + * @return instance of the singleton + */ + public static AlgorithmHandler getInstance() { + if (instance == null) { + instance = new AlgorithmHandler(); + // Default Constructor + } + + return instance; + } + + @Nullable + public Line runAlgorithmByType(final Algorithm.Type type, final Set setOfLines, + final BooleanProperty guiFlag) { + + if (guiFlag != null) { + guiFlag.setValue(true); + } + + //get the instance + Algorithm algorithm = algorithmMapping.get(type); + algorithm.setPresenter(Presenter.getInstance()); + algorithm.setInput(setOfLines); + Logging.logDebug("run Algorithm: " + algorithm.getClass().getSimpleName()); + + //get the executor + ExecutorService executor = Presenter.getInstance().getExecutor(); + ExecutorCompletionService completionService = new ExecutorCompletionService<>( + executor); + + completionService.submit(algorithm); + try { + + return completionService.take().get(); + } catch (InterruptedException e) { + Logging.logError( + "Interrupt Exception while waiting for result of Algorithm: " + algorithm + .getType(), e); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + Logging.logError( + "Execution Exception while computing the result of Algorithm: " + algorithm + .getType(), e); + } finally { + if (guiFlag != null) { + guiFlag.setValue(false); + } + } + return null; + } + + @Nullable + public Line runAlgorithmByType(final Algorithm.Type type, final Set setOfLines) { + return runAlgorithmByType(type, setOfLines, null); + } + + private void lookUp() { + Logging.logDebug("Lookup for Algorithm Classes."); + ServiceLoader load = ServiceLoader.load(Algorithm.class); + load.reload(); + algorithmMapping = new EnumMap<>(Algorithm.Type.class); + load.forEach(algorithm -> { + algorithmMapping.put(algorithm.getType(), algorithm); + Logging.logDebug("Found: " + algorithm.getClass().getSimpleName()); + }); + } + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimator.java new file mode 100644 index 0000000..feb9ed5 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimator.java @@ -0,0 +1,467 @@ +package de.wwwu.awolf.presenter.algorithms.advanced; + +import de.wwwu.awolf.model.Interval; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.Point; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.IntersectionComputer; +import de.wwwu.awolf.presenter.util.Logging; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class LeastMedianOfSquaresEstimator implements Algorithm { + + private static final Algorithm.Type type = Type.LMS; + + private List setOfLines; + private int n; + private double quantileError; + private int kPlus; + private int kMinus; + private Queue intervals; + private Interval subSlabU1; + private Interval subSlabU2; + private Line sigmaMin; + private double heightsigmaMin; + private double intersectionsPoint; + private double constant = 0.5; + + private double slope; + private double yInterception; + private Flow.Subscriber subscriber; + + /** + * Algorithmus zum berechnen des LMS-Schätzers + *

+ * Paper: Mount, David M, Nathan S Netanyahu, Kathleen Romanik, Ruth Silverman und Angela Y Wu „A practical approximation algorithm for the LMS line estimator“. 2007 Computational statistics & + * data Analysis 51.5, S. 2461–2486 + */ + @Override + public Line call() { + this.n = setOfLines.size(); + double qPlus = 0.5; + quantileError = 0.1; + double qMinus = qPlus * (1 - quantileError); + kMinus = (int) Math.ceil(n * qMinus); + kPlus = (int) Math.ceil(n * qPlus); + + Logging.logInfo("=== S T A R T - L M S ==="); + long start; + long end; + start = System.currentTimeMillis(); + + //(2.) Let U <- (-inf, inf) be the initial active intervals... + Comparator comparator = (o1, o2) -> { + if (o1.getDistance() < o2.getDistance()) { + return -1; + } + if (o1.getDistance() > o2.getDistance()) { + return 1; + } else { + return 0; + } + }; + intervals = new PriorityQueue<>(comparator); + intervals.add(new Interval(-10000, 10000)); + heightsigmaMin = Double.MAX_VALUE; + + //(3.) Apply the following steps as long as the exists active intervals + Interval interval; + while (!this.intervals.isEmpty()) { + interval = this.intervals.peek(); + if (interval.getActivity()) { + //(a.) Select any active Interval and calc. the inversions + int numberOfIntersections = countInversions(interval); + + //(b.) apply plane sweep + if ((constant * n) >= numberOfIntersections) { + sigmaMin = pseudoSweep(interval); + } else { + //(c.) otherwise.... + // get random intersections point... + Collection tmpIntersections = IntersectionComputer + .getInstance() + .compute(setOfLines, interval.getLower(), interval.getUpper()); + boolean found = false; + for (Point tmpIntersection : tmpIntersections) { + if (tmpIntersection.getX() > interval.getLower() + && tmpIntersection.getX() < interval.getUpper()) { + intersectionsPoint = tmpIntersection.getX(); + found = true; + break; + } + } + if (found) { + splitActiveSlab(intersectionsPoint, interval); + //(d.) this may update sigma min + upperBound(intersectionsPoint); + //(e.) for i={1,2}, call lower bound(Ui) + lowerBound(subSlabU1); + lowerBound(subSlabU2); + + if (subSlabU1.getActivity()) { + this.intervals.add(subSlabU1); + } + if (subSlabU2.getActivity()) { + this.intervals.add(subSlabU2); + } + + } else { + this.intervals.poll(); + } + } + } else { + this.intervals.remove(interval); + } + } + end = System.currentTimeMillis(); + Logging.logInfo("=== E N D - L M S === " + ((end - start) / 1000)); + + return pepareResult(); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new LinkedList<>(lines); + setN(this.setOfLines.size()); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + subscribe(presenter); + } + + + /** + * Liefert die Anzahl der Schnittpunkte in einem Intervall + * + * @param interval Intervall + * @return Anzahl der Schnittpunkte + */ + public int countInversions(Interval interval) { + return IntersectionComputer.getInstance() + .compute(setOfLines, interval.getLower(), interval.getUpper()).size(); + } + + + /** + * In der Literatur wird ein planesweep vorrausgesetzt um in einem Intervall das minimale kMinus Segment zu identifizieren. An dieser Stelle wird eine naivere Lösung verwendet, mithilfe der + * Schleife werden alle Schnittpunkte betrachtet und bei passenden x-Koordinaten werden wird die vertikale Gerade auf kMinus Segmente untersucht. + * + * @param interval + * @return + */ + public Line pseudoSweep(Interval interval) { + + //initialisiere die x-Queue mit den 2D Punkten und sortiere nach x-Lexikographischer Ordnung + List xQueue = new ArrayList<>(); + Collection points = IntersectionComputer.getInstance() + .compute(setOfLines, interval.getLower(), interval.getUpper()); + for (Point point : points) { + if (point.getX() >= interval.getLower() && point.getX() < interval.getUpper()) { + xQueue.add(point); + } + } + Collections.sort(xQueue); + + Line bracelet = sigmaMin; + double heightOfBracelet = heightsigmaMin; + + for (Point current : xQueue) { + Double[] currentBracelet = calcKMinusBracelet(current, kMinus); + + if (currentBracelet == null) { + continue; + } else if (currentBracelet[0] < heightOfBracelet) { + heightOfBracelet = currentBracelet[0]; + bracelet = new Line(current.getX(), current.getX(), currentBracelet[1], + currentBracelet[2]); + } + } + + interval.setActivity(false); + return bracelet; + } + + /** + * Diese Methode spaltet den aktiven Interval an der x Koordinate point. Es werden zwei neue Slabs erzeugt. + * + * @param point x Koordinate an der, der Split geschieht. + */ + public void splitActiveSlab(double point, Interval active) { + subSlabU1 = new Interval(active.getLower() + 0.01, point); + subSlabU2 = new Interval(point, active.getUpper()); + + this.intervals.remove(active); + } + + /** + * Zu einer vertikalen Gerade werden kMinus Segmente erzeugt und geprüft ob eine bessere Lösung als SigmaMin vorliegt. + * + * @param point x-Koordinate zur Konstruktion der vertikalen Gerade + */ + public void upperBound(double point) { + + double height = heightsigmaMin; + double tmpHeight; + List sortedLineSequence = getEjValues(point); + + int itnbr = ((n - kMinus) + 1); + for (int i = 0; i < itnbr; i++) { + tmpHeight = sortedLineSequence.get((i + kMinus) - 1) - sortedLineSequence.get(i); + if (tmpHeight < height) { + height = tmpHeight; + } + + if (height < heightsigmaMin) { + heightsigmaMin = height; + if (sigmaMin != null) { + sigmaMin.setEndPoints(point, sortedLineSequence.get(i) + , point, sortedLineSequence.get((i + kMinus) - 1)); + } else { + sigmaMin = new Line(point, point, sortedLineSequence.get(i), + sortedLineSequence.get((i + kMinus) - 1)); + } + } + } + + } + + /** + * Die Methode prüft ob das übergebene Intervall die untere Schranke einer potenziellen Lösung erfüllt. + * + * @param pslab übergebene Intervall + */ + public void lowerBound(Interval pslab) { + + int[] alpha = new int[n]; + int[] beta = new int[n]; + int strictlyGreater = 0; + + //Teil I. + List umaxList; + List uminList; + + //y koordinaten der Schnittpunkte + List lines = new ArrayList<>(); + for (Line p : setOfLines) { + lines.add( + new Line(pslab.getLower(), pslab.getUpper(), + ((pslab.getLower() * p.getM()) + p.getB()), + ((pslab.getUpper() * p.getM()) + p.getB()))); + } + + umaxList = getEjValues(pslab.getUpper()); + uminList = getEjValues(pslab.getLower()); + + for (int i = 0; i < n; i++) { + Line level = new Line(pslab.getLower(), pslab.getUpper(), uminList.get(i), + umaxList.get(i)); + for (Line line : lines) { + if ((line.getY1() < level.getY1()) && (line.getY2() < level.getY2())) { + alpha[i]++; + } + + if ((line.getY1() > level.getY1()) && (line.getY2() > level.getY2())) { + strictlyGreater++; + } + } + beta[i] = n - (alpha[i] + strictlyGreater); + strictlyGreater = 0; + } + + //Teil II. + int i = 0; + double h; + pslab.setActivity(false); + for (int j = 0; j < n; j++) { + while ((i < n && (Math.abs(beta[i] - alpha[j]) < kPlus))) { + i++; + } + + if (i >= n) { + pslab.setActivity(false); + break; + } else { + h = Math.min(Math.abs(uminList.get(j) - uminList.get(i)), + Math.abs(umaxList.get(j) - umaxList.get(i))); + double error = 0.01; + if (((1 + error) * h) < heightsigmaMin) { + pslab.setActivity(true); + return; + } + } + i = 0; + } + + } + + /** + * Berechnet die Schnittpunkte der Geraden und der vertikalen Gerade u. Im paper sind diese Werte als e_j Werte bekannt. + * + * @param u vertikale Gerade + * @return Liste der Schnittpunkte (da u bekannt werden nur die y Werte zurück gegeben) + */ + public List getEjValues(double u) { + + List ret = new ArrayList<>(); + + for (Line p : setOfLines) { + ret.add((p.getM() * u) + p.getB()); + } + + Collections.sort(ret); + + return ret; + } + + /** + * Die Funktion berechnet anhand einer vertikalen Gerade x = px das sogenannte kleinste kMinus Bracelet. Mit anderen Worten es wird eine vertikale Teilgerade berechnet die mindestens kMinus + * Geraden schneidet und dabei minimal ist. + * + * @param px Koordinate um die "vertikale Gerade" zu simulieren. + * @return Das Array enthält höhe des Bracelet, e_j und e_(j + kMinus - 1) + */ + public Double[] calcKMinusBracelet(Point px, int kMinusValue) { + + //y Koordinaten für das kMinus brecalet + List intersections = new LinkedList<>(); + for (Line line : setOfLines) { + intersections.add((px.getX() * line.getM()) + line.getB()); + } + if (intersections.size() >= kMinusValue) { + Collections.sort(intersections); + double height = Math + .abs(intersections.get(0) - intersections.get(kMinusValue - 1)); + return new Double[]{height, intersections.get(0), + intersections.get(kMinusValue - 1)}; + } else { + return null; + } + + } + + private Line pepareResult() { + if (this.subscriber != null) { + double m = ((getSigmaMin().getX2() + getSigmaMin().getX1()) * 0.5); + double b = (getSigmaMin().getY2() + getSigmaMin().getY1()) * -0.5; + + slope = m; + yInterception = b; + + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(m, b)); + this.subscriber.onNext(data); + } else { + double m = (getSigmaMin().getX2() + getSigmaMin().getX1()) * 0.5; + double b = (getSigmaMin().getY2() + getSigmaMin().getY1()) * -0.5; + + slope = m; + yInterception = b; + } + + return new Line(getSlope(), getYInterception()); + } + + /** + * @return Anzahl der Geraden + */ + public int getN() { + return n; + } + + /** + * @param n Anzahl der Geraden + */ + public void setN(int n) { + this.n = n; + } + + /** + * @param quantileError Parameter quantile Fehler + */ + public void setQuantileError(double quantileError) { + this.quantileError = quantileError; + } + + /** + * @param kMinus kMinus index + */ + public void setkMinus(int kMinus) { + this.kMinus = kMinus; + } + + /** + * @return duale Streifen Sigma_min + */ + public Line getSigmaMin() { + return sigmaMin; + } + + /** + * @param sigmaMin duale Streifen Sigma_min + */ + public void setSigmaMin(Line sigmaMin) { + this.sigmaMin = sigmaMin; + } + + /** + * @param heightsigmaMin höhe von Sigma_min + */ + public void setHeightsigmaMin(double heightsigmaMin) { + this.heightsigmaMin = heightsigmaMin; + } + + /** + * @param constant Parameter Konstante + */ + public void setConstant(Double constant) { + this.constant = constant; + } + + /** + * @return Steigung + */ + public Double getSlope() { + return slope; + } + + /** + * @return y-Achsenabschnitt + */ + public Double getYInterception() { + return yInterception; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/RepeatedMedianEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/RepeatedMedianEstimator.java new file mode 100644 index 0000000..a31f992 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/RepeatedMedianEstimator.java @@ -0,0 +1,356 @@ +package de.wwwu.awolf.presenter.algorithms.advanced; + +import de.wwwu.awolf.model.Interval; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.Point; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import de.wwwu.awolf.presenter.util.IntersectionComputer; +import de.wwwu.awolf.presenter.util.Logging; +import de.wwwu.awolf.presenter.util.RandomSampler; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Flow; + + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class RepeatedMedianEstimator implements Algorithm { + + + private static final Algorithm.Type type = Type.RM; + + private AbstractPresenter presenter; + private List setOfLines; + private Interval interval; + private Interval original; + + //in der Literatur als L_i, C_i, und R_i bekannt + private int countLeftSlab; + private int countCenterSlab; + private int countRightSlab; + + //in der Literatur als L_i, C_i, und R_i bekannt + private Set intersectionsInLeftSlab; + private Set intersectionsInCenterSlab; + private Set intersectionsInRightSlab; + + private double r; + private int n; + private double k; + private double kLow; + private double kHigh; + private double beta; + + private double slope; + private double yInterception; + private Flow.Subscriber subscriber; + + + /** + * Führt den Algortihmus zur Berechnung des RM-Schätzers durch. + *

+ * Paper: Matousek, Jiri, D. M. Mount und N. S. Netanyahu „Efficient Randomized Algorithms for the Repeated Median Line Estimator“. 1998 Algorithmica 20.2, S. 136–150 + */ + public Line call() { + + n = setOfLines.size(); + interval = new Interval(-10000, 10000); + original = new Interval(-10000, 10000); + beta = 0.5; + + intersectionsInLeftSlab = new HashSet<>(); + intersectionsInCenterSlab = new HashSet<>(); + intersectionsInRightSlab = new HashSet<>(); + + intersectionsInLeftSlab.add(new Point(0d, 0d)); + intersectionsInCenterSlab.add(new Point(0d, 0d)); + intersectionsInCenterSlab.add(new Point(0d, 0d)); + + for (int i = 0; i < n; i++) { + countLeftSlab = 0; + countRightSlab = 0; + countCenterSlab = setOfLines.size(); + } + + Logging.logInfo("=== S T A R T - R M ==="); + long start; + long end; + start = System.currentTimeMillis(); + double thetaLow = 0; + double thetaHigh = 0; + + while (countCenterSlab > 1) { + n = countCenterSlab; + r = Math.ceil(Math.pow(n, beta)); + List lines = RandomSampler.run(setOfLines, r); + + //Für jede Gerade aus der Stichprobe wird der Schnittpunkt mit der medianen + //x-Koordinate bestimmt + List medianIntersectionAbscissas = new ArrayList<>(); + final double lowerBound = thetaLow; + final double upperBound = thetaHigh; + if (!lines.isEmpty()) { + lines.forEach(line -> medianIntersectionAbscissas + .add(estimateMedianIntersectionAbscissas(lines, line))); + } + + //Rang vom RM-Wert in C + k = Math.max(1, Math.min(n, (Math.ceil(n * 0.5) - countLeftSlab))); + + //berechne k_lo und k_hi + computeSlabBorders(); + + //Berechne die Elemente mit dem Rang Theta_lo und Theta_hi + Collections.sort(medianIntersectionAbscissas); + thetaLow = FastElementSelector + .randomizedSelect(medianIntersectionAbscissas, kLow); + thetaHigh = FastElementSelector + .randomizedSelect(medianIntersectionAbscissas, kHigh); + + //Für jede Gerade in C wird die Anzahl der Schnittpunkte die im Intervall liegen hochgezählt + countNumberOfIntersectionsAbscissas(thetaLow, thetaHigh); + + //verkleinere das Intervall + contractIntervals(thetaLow, thetaHigh); + + } + + end = System.currentTimeMillis(); + Logging.logInfo("=== E N D - R M === " + ((end - start) / 1000)); + + return pepareResult(thetaLow, thetaHigh); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new LinkedList<>(lines); + this.n = setOfLines.size(); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + this.presenter = presenter; + subscribe(presenter); + } + + + /** + * Berechnet die mediane x-Koordinate über den Schnittpunkten. + * + * @param sampledLine Stichprobe von Geraden + * @return mediane x-Koordinate über den Schnittpunkten + */ + public Double estimateMedianIntersectionAbscissas(List lines, Line sampledLine) { + + List intersections = IntersectionComputer.getInstance() + .calculateIntersectionAbscissas(lines, sampledLine, original.getLower(), + original.getUpper()); + List left = IntersectionComputer.getInstance() + .calculateIntersectionAbscissas(lines, sampledLine, original.getLower(), + interval.getLower()); + List center = IntersectionComputer.getInstance() + .calculateIntersectionAbscissas(lines, sampledLine, interval.getLower(), + interval.getUpper()); + + double ki = Math.ceil((n - 1) * 0.5) - left.size(); + double i = (Math.ceil((Math.sqrt(n) * ki) / center.size())); + int accessIndex; + if (i < 0) { + accessIndex = 0; + } else if (i >= intersections.size() && !intersections.isEmpty()) { + accessIndex = intersections.size() - 1; + } else { + accessIndex = (int) i; + } + + return FastElementSelector.randomizedSelect(intersections, accessIndex); + } + + /** + * Berechnet die potenziell neuen Intervallgrenzen. + */ + public void computeSlabBorders() { + kLow = Math.max(1, Math.floor(((r * k) / (countCenterSlab)) + - ((3 * Math.sqrt(r)) * (0.5)))); + kHigh = Math.min(r, Math.floor(((r * k) / (countCenterSlab)) + + ((3 * Math.sqrt(r)) * (0.5)))); + } + + + /** + * Berechnet die Anzahl der Schnittpunkte pro Bereich. Insgesammt gibt es drei Bereiche: Im Intervall => (a,b], vor dem Intervall => (a', a], hinter dem Intervall => (b, b']. + */ + public void countNumberOfIntersectionsAbscissas(final double lower, final double upper) { + IntersectionComputer instance = IntersectionComputer.getInstance(); + + intersectionsInLeftSlab = new HashSet<>( + instance.compute(setOfLines, interval.getLower(), lower)); + intersectionsInCenterSlab = new HashSet<>(instance.compute(setOfLines, lower, upper)); + intersectionsInRightSlab = new HashSet<>( + instance.compute(setOfLines, upper, interval.getUpper())); + + int tmp = new HashSet<>( + instance.compute(setOfLines, interval.getLower(), interval.getUpper())) + .size(); + countLeftSlab = intersectionsInLeftSlab.size(); + countCenterSlab = intersectionsInCenterSlab.size(); + countRightSlab = intersectionsInRightSlab.size(); + } + + /** + * Verkleinert das aktuelle Intervall. Eines der drei Bereiche wird als neues Intervall gewählt. Auf diesem Intervall werden dann in der nächsten Iteration wieder drei Bereiche bestimmt. + */ + public void contractIntervals(final double lower, final double upper) { + double max = Math.max(countLeftSlab, Math.max(countCenterSlab, countRightSlab)); + + boolean newIntervalIsC = + countLeftSlab < Math.ceil(n * 0.5) + && Math.ceil(n * 0.5) <= countLeftSlab + countCenterSlab; + boolean newIntervalIsL = Math.ceil(n * 0.5) <= countLeftSlab; + boolean newIntervalIsR = + countLeftSlab + countCenterSlab < Math.ceil(n * 0.5) && Math.ceil(n * 0.5) <= ( + countLeftSlab + + countCenterSlab + countRightSlab); + + //wähle C als C + if (newIntervalIsC) { + interval.setLower(lower); + interval.setUpper(upper); + } else if (newIntervalIsL) { + interval.setUpper(lower); + } else if (newIntervalIsR) { + interval.setLower(upper); + } + } + + private Line pepareResult(final double thetaLow, final double thetaHigh) { + slope = thetaLow; + List potentialYInterceptions = new ArrayList<>(); + setOfLines.forEach(line -> { + potentialYInterceptions.add(line.getB() - (slope * line.getM())); + }); + + yInterception = FastElementSelector.randomizedSelect(potentialYInterceptions, + Math.floor(potentialYInterceptions.size() * 0.5)); + + if (this.subscriber != null) { + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(getSlope(), getyInterception())); + this.subscriber.onNext(data); + } + + return new Line(getSlope(), getyInterception()); + } + + /** + * @return Anzahl der Geraden + */ + public Integer getN() { + return n; + } + + /** + * @param n Anzahl der Geraden + */ + public void setN(Integer n) { + this.n = n; + } + + /** + * @param beta Parameter Beta + */ + public void setBeta(Double beta) { + this.beta = beta; + } + + /** + * @return Steigung + */ + public Double getSlope() { + return slope; + } + + /** + * @return y-Achsenabschnitt + */ + public Double getyInterception() { + return yInterception; + } + + /** + * @return temporäres untere Intervallgrenze + */ + public Double getkLow() { + return kLow; + } + + /** + * @param kLow temporäres untere Intervallgrenze + */ + public void setkLow(Double kLow) { + this.kLow = kLow; + } + + /** + * @return temporäres oberes Intervallgrenze + */ + public Double getkHigh() { + return kHigh; + } + + /** + * @param kHigh temporäres oberes Intervallgrenze + */ + public void setkHigh(Double kHigh) { + this.kHigh = kHigh; + } + + /** + * @return verteilung der Punkte + */ + public int getCountLeftSlab() { + return countLeftSlab; + } + + /** + * @return verteilung der Punkte + */ + public int getCountCenterSlab() { + return countCenterSlab; + } + + /** + * @return verteilung der Punkte + */ + public int getCountRightSlab() { + return countRightSlab; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} + + diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimator.java new file mode 100644 index 0000000..477a4b2 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimator.java @@ -0,0 +1,248 @@ +package de.wwwu.awolf.presenter.algorithms.advanced; + +import de.wwwu.awolf.model.Interval; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.Point; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.BinomialCoeffizient; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import de.wwwu.awolf.presenter.util.IntersectionComputer; +import de.wwwu.awolf.presenter.util.RandomSampler; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class TheilSenEstimator implements Algorithm { + + + private static final Algorithm.Type type = Type.TS; + + private final double POSITIV_INF = 9999.0; + private final double NEGATIV_INF = -9999.0; + private final double EPSILON = 0.00001; + private List setOfLines; + //Hilfsvariablen (siehe original Paper) + private double j; + private int jA; + private int jB; + private double r; + private int n; + private double N; + private int k; + //Intervall und die temporaeren Grenzen + private Interval interval; + private double aVariant; + private double bVariant; + private double slope; + private double yInterception; + private Flow.Subscriber subscriber; + private AbstractPresenter presenter; + + + /** + * Randomisierter Algorithmus zur Berechnung des Theil-Sen Schätzers. Algorithmus stammt aus dem Paper: "Jiri Matousek, Randomized optimal algorithm for slope selection, Information Processing + * Letters 39 (1991) 183-187 + */ + public Line call() { + + this.n = this.setOfLines.size(); + this.N = BinomialCoeffizient.run(n, 2); + //this.k = Integer.valueOf((int) (N * 0.5)) - 1; + this.k = (int) (N / 2); + + interval = new Interval(NEGATIV_INF, POSITIV_INF); + //damit eine initiale Ordnung herscht + //Collections.sort(intervalIntersections); + + r = n; + List intervalIntersections = new LinkedList<>(IntersectionComputer.getInstance() + .compute(setOfLines, interval.getLower(), interval.getUpper())); + while (true) { + if (this.N <= n + || (Math.abs(interval.getUpper() - interval.getLower())) < EPSILON) { + break; + } else { + //Anzahl der Schnittpunkte im Intervall [-Inf, a) + int numberOfIntersections = getIntervalSize(NEGATIV_INF, + interval.getLower()); + + //Randomized Interpolating Search + j = (r / N) * (double) (k - numberOfIntersections); + jA = (int) Math.max(1, Math.floor(j - (1.5 * Math.sqrt(r)))); + jB = (int) Math.min(r, Math.floor(j + (1.5 * Math.sqrt(r)))); + + + /* Suche nach einem passenderen und kleineren Intervall + Schleife terminiert wenn die das k-te Elemnet zwischen aVariant und bVariant liegt und + das Intrvall weniger als 11*N / sqrt(r) Elemente besitzt */ + do { + //zufällige Stichprobe + List sampledIntersections = RandomSampler + .run(intervalIntersections, r); + + aVariant = FastElementSelector + .randomizedSelect(getIntersectionAbscissas(sampledIntersections), + jA); + bVariant = FastElementSelector + .randomizedSelect(getIntersectionAbscissas(sampledIntersections), + jB); + } while (!checkCondition()); + + interval.setLower(aVariant); + interval.setUpper(bVariant); + intervalIntersections = getOpenIntervalElements(interval.getLower(), + interval.getUpper()); + N = getIntervalSize(interval.getLower(), interval.getUpper()); + } + } + + return pepareResult(); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new LinkedList<>(lines); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + this.presenter = presenter; + subscribe(presenter); + } + + private List getIntersectionAbscissas(List interections) { + List abscissas = new ArrayList<>(); + interections.forEach(e -> abscissas.add(e.getX())); + return abscissas; + } + + + /** + * Diese Funktion überprüft ob die Bedingung für das Interval erfüllt ist. Dabei muss der k-te Schnittpunkt in diesem Interval enthalten sein. des weiteren soll die Anzahl der Schnittpunkte im + * Interval kleiner oder gleich dem Term: (11*N)/sqrt(r) sein. + * + * @return Boolscher Wert ob die Bedingung erfüllt ist + */ + private Boolean checkCondition() { + //Double kthElement = FastElementSelector.randomizedSelect(xCoordinates, k); + //Boolean cond1 = (kthElement > aVariant) && (kthElement <= bVariant); + + int lowerCount = getIntervalSize(NEGATIV_INF, aVariant); + int higherCount = getIntervalSize(NEGATIV_INF, bVariant); + + Boolean conda = k > lowerCount; + Boolean condb = k <= higherCount; + + Boolean cond1 = conda && condb; + + Boolean cond2 = (higherCount - lowerCount) <= ((11 * N) / Math.sqrt(r)); + + return (cond1 && cond2) || (aVariant == bVariant); + } + + + /** + * Berechne wieviele von den Schnittpunkten in dem Interval zwischen a und + * b enthalten sind. + * + * @param a untere Grenze des Intervals + * @param b obere Grenze des Intrvals + * @return Anzahl der Schnittpunkte im Interval [a,b) + */ + public int getIntervalSize(double a, double b) { + return getOpenIntervalElements(a, b).size(); + } + + /** + * Berechne wieviele von den Schnittpunkten in dem Interval zwischen a und + * b enthalten sind. Zusätzlich werden diese Schnittpunkte in einer Liste + * festgehalten und diese werden zurückgeliefert. + * + * @param a untere Grenze des Intervals + * @param b obere Grenze des Intrvals + * @return Liste der Schnittpunkte die im Interval (a,b) vertreten sind + */ + public List getOpenIntervalElements(double a, double b) { + Collection intersections = IntersectionComputer.getInstance() + .compute(setOfLines, a, b); + return new ArrayList<>(intersections); + } + + private Line pepareResult() { + double m, x; + double b, y; + + List resultSt = getOpenIntervalElements(interval.getLower(), + interval.getUpper()); + List resultAbscissas = new ArrayList<>(); + + for (Point p : resultSt) { + resultAbscissas.add(p.getX()); + } + + List yCoords = new ArrayList<>(); + + for (Point p : getOpenIntervalElements(interval.getLower(), interval.getUpper())) { + yCoords.add(p.getY()); + } + + double pseudoIndex = getIntervalSize(NEGATIV_INF, interval.getLower()) * 1.0; + m = FastElementSelector.randomizedSelect(resultAbscissas, k - pseudoIndex); + + Set unique = new LinkedHashSet<>(yCoords); + yCoords.clear(); + yCoords.addAll(unique); + b = FastElementSelector.randomizedSelect(yCoords, yCoords.size() * 0.5) * -1; + slope = m; + yInterception = b; + + if (this.subscriber != null) { + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(m, b)); + this.subscriber.onNext(data); + } + + return new Line(getSlope(), getYInterception()); + } + + /** + * @return Steigung + */ + public Double getSlope() { + return slope; + } + + /** + * @return y-Achsenabschnitt + */ + public Double getYInterception() { + return yInterception; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivLeastMedianOfSquaresEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivLeastMedianOfSquaresEstimator.java new file mode 100644 index 0000000..4efcfb7 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivLeastMedianOfSquaresEstimator.java @@ -0,0 +1,143 @@ +package de.wwwu.awolf.presenter.algorithms.naiv; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import de.wwwu.awolf.presenter.util.Logging; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 15.09.2017. + */ +public class NaivLeastMedianOfSquaresEstimator implements Algorithm { + + private static final Algorithm.Type type = Type.NAIVE_LMS; + private List setOfLines = new ArrayList<>(); + + private int n; + private double ds, b, m; + private Flow.Subscriber subscriber; + private AbstractPresenter presenter; + + + /** + * Crude Algorithmus zum berechnen des LSM-Schätzers. + */ + private Line crudeAlg() { + ds = Double.MAX_VALUE; + b = 0d; + m = 0d; + List triple = new ArrayList<>(); + double beta; + double alpha; + double dijk; + Logging.logInfo("=== S T A R T - naiv L M S ==="); + long start; + long end; + start = System.currentTimeMillis(); + for (Line i : setOfLines) { + for (Line j : setOfLines) { + for (Line k : setOfLines) { + triple.add(i); + triple.add(j); + triple.add(k); + Collections.sort(triple); + beta = + (triple.get(0).getB() - triple.get(2).getB()) / ( + triple.get(0).getM() - triple.get(2) + .getM()); + alpha = + (triple.get(1).getB() + triple.get(2).getB() - (beta * ( + triple.get(1).getM() + triple + .get(2).getM()))) * 0.5; + dijk = f(alpha, beta); + if (dijk < ds) { + ds = dijk; + b = alpha; + m = beta; + } + triple.clear(); + } + } + } + end = System.currentTimeMillis(); + Logging.logInfo("=== E N D - naiv L M S ==="); + Logging.logInfo("Slope: " + getSlope() + ", y-Interception: " + getYInterception()); + + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(getSlope(), getYInterception())); + this.subscriber.onNext(data); + + return new Line(getSlope(), getYInterception()); + } + + /** + * Berechnet die Gerade mit dem medianen Fehler + * + * @param a y-Achsenabschnitt + * @param b Steigung + * @return medianer Fehler + */ + private Double f(double a, double b) { + List res = new ArrayList<>(); + for (Line p : setOfLines) { + res.add(Math.abs(p.getB() - (a + b * p.getM()))); + } + return FastElementSelector.randomizedSelect(res, res.size() * 0.5); + } + + @Override + public Line call() { + return crudeAlg(); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new LinkedList<>(lines); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + this.presenter = presenter; + subscribe(presenter); + } + + /** + * @return y-Achsenabschnitt + */ + public Double getYInterception() { + return b * -1; + } + + /** + * @return Steigung + */ + public Double getSlope() { + return m * -1; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivRepeatedMedianEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivRepeatedMedianEstimator.java new file mode 100644 index 0000000..578ddce --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivRepeatedMedianEstimator.java @@ -0,0 +1,165 @@ +package de.wwwu.awolf.presenter.algorithms.naiv; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.Point; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import de.wwwu.awolf.presenter.util.Logging; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 15.09.2017. + */ +public class NaivRepeatedMedianEstimator implements Algorithm { + + private static final Algorithm.Type type = Type.NAIVE_RM; + + private List setOfLines; + private Map> slopesPerLine; + private Map> interceptPerLine; + private List xMedians; + private List yMedians; + private double medianX; + private double medianY; + private Flow.Subscriber subscriber; + private AbstractPresenter presenter; + + @Override + public Line call() { + + slopesPerLine = new HashMap<>(); + interceptPerLine = new HashMap<>(); + xMedians = new ArrayList<>(); + yMedians = new ArrayList<>(); + + //init the List for the slopes + Logging.logInfo("=== S T A R T - naiv R M ==="); + long start; + long end; + start = System.currentTimeMillis(); + for (Line leq : setOfLines) { + slopesPerLine.computeIfAbsent(leq.getId(), k -> new ArrayList<>()); + interceptPerLine.computeIfAbsent(leq.getId(), k -> new ArrayList<>()); + } + + //calculate all slopes for each line + Point ret; + for (int i = 0; i < setOfLines.size(); i++) { + for (int j = i + 1; j < setOfLines.size(); j++) { + ret = calculateLine(setOfLines.get(i), setOfLines.get(j)); + slopesPerLine.get(setOfLines.get(i).getId()).add(ret.getX()); + interceptPerLine.get(setOfLines.get(i).getId()).add(ret.getY()); + } + } + + //berechne mediane Steigung + for (String l : slopesPerLine.keySet()) { + List list = slopesPerLine.get(l); + int size = list.size() / 2; + if (size > 0) { + double medianX = FastElementSelector.randomizedSelect(list, size); + xMedians.add(medianX); + } + } + + //berechne medianen y-Achsenabschnitt + for (String l : interceptPerLine.keySet()) { + List list = interceptPerLine.get(l); + int size = list.size() / 2; + if (size > 0) { + double medianY = FastElementSelector.randomizedSelect(list, size); + yMedians.add(medianY); + } + } + + medianX = FastElementSelector.randomizedSelect(xMedians, xMedians.size() * 0.5); + medianY = FastElementSelector.randomizedSelect(yMedians, yMedians.size() * 0.5); + end = System.currentTimeMillis(); + Logging.logInfo("=== E N D - naiv R M ==="); + Logging.logInfo("Slope: " + getSlope() + ", y-Interception: " + getYInterception()); + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(getSlope(), getYInterception())); + this.subscriber.onNext(data); + return new Line(getSlope(), getYInterception()); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new ArrayList<>(lines); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + this.presenter = presenter; + subscribe(presenter); + } + + /** + * Berechnet die Geraden zwischen zwei Punkten im dualen Raum. + * + * @param startPoint Gerade 1 => Startpunkt mit den Koordianten (m,b) + * @param endPoint Gerade 2 => Endpunkt mit den Koordianten (m', b') + * @return + */ + private Point calculateLine(Line startPoint, Line endPoint) { + double xi; + double xj; + double yi; + double yj; + + if (endPoint.getM() > startPoint.getM()) { + xi = startPoint.getM(); + yi = startPoint.getB(); + xj = endPoint.getM(); + yj = endPoint.getB(); + } else { + xj = startPoint.getM(); + yj = startPoint.getB(); + xi = endPoint.getM(); + yi = endPoint.getB(); + } + + double m = (yj - yi) / (xj - xi); + double b = ((xj * yi) - (xi * yj)) / (xj - xi); + return new Point(m, b); + } + + /** + * @return Steigung + */ + public double getSlope() { + return medianX * -1; + } + + /** + * @return y-Achsenabschnitt + */ + public double getYInterception() { + return medianY * -1; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivTheilSenEstimator.java b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivTheilSenEstimator.java new file mode 100644 index 0000000..ad44db7 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/algorithms/naiv/NaivTheilSenEstimator.java @@ -0,0 +1,114 @@ +package de.wwwu.awolf.presenter.algorithms.naiv; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.communication.AlgorithmData; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import de.wwwu.awolf.presenter.util.Logging; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 15.09.2017. + */ +public class NaivTheilSenEstimator implements Algorithm { + + private static final Algorithm.Type type = Type.NAIVE_TS; + + private List setOfLines; + private double slope; + private double yInterception; + private Flow.Subscriber subscriber; + private AbstractPresenter presenter; + + @Override + public Line call() { + this.slope = 0d; + this.yInterception = 0d; + Logging.logInfo("=== S T A R T - naiv T S ==="); + long start; + long end; + start = System.currentTimeMillis(); + List slopesList = new ArrayList<>(); + int cnt = 0; + for (int i = 0; i < setOfLines.size(); i++) { + double x = setOfLines.get(i).getM(); + double y = setOfLines.get(i).getB(); + + for (int j = i + 1; j < setOfLines.size(); j++) { + if (x != setOfLines.get(j) + .getM()) { // x must be different, otherwise slope becomes infinite + Double slope = + (setOfLines.get(j).getB() - y) / (setOfLines.get(j).getM() - x); + slopesList.add(slope); + ++cnt; + } + } + } + + List list1 = new ArrayList<>(); + List list2 = new ArrayList<>(); + for (Line line : setOfLines) { + list1.add(line.getM()); + list2.add(line.getB()); + } + double median1 = FastElementSelector.randomizedSelect(list1, list1.size() * 0.5); + double median2 = FastElementSelector.randomizedSelect(list2, list2.size() * 0.5); + slope = FastElementSelector.randomizedSelect(slopesList, slopesList.size() * 0.5); + yInterception = median2 - slope * median1; + end = System.currentTimeMillis(); + Logging.logInfo("=== E N D - naiv T S ==="); + Logging.logInfo("Slope: " + getSlope() + ", y-Interception: " + getYInterception()); + AlgorithmData data = new AlgorithmData(); + data.setAlgorithmType(getType()); + data.setType(SubscriberType.ALGORITHM); + data.setLineData(new Line(getSlope(), getYInterception())); + this.subscriber.onNext(data); + return new Line(getSlope(), getYInterception()); + } + + @Override + public void setInput(Set lines) { + this.setOfLines = new ArrayList<>(lines); + } + + @Override + public Type getType() { + return type; + } + + @Override + public void setPresenter(AbstractPresenter presenter) { + this.presenter = presenter; + subscribe(presenter); + } + + + /** + * @return Steigung + */ + public double getSlope() { + return slope * -1; + } + + /** + * @return y-Achsenabschnitt + */ + public double getYInterception() { + return yInterception * -1; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/data/DataHandler.java b/src/main/java/de/wwwu/awolf/presenter/data/DataHandler.java new file mode 100644 index 0000000..66a7048 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/DataHandler.java @@ -0,0 +1,89 @@ +package de.wwwu.awolf.presenter.data; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.AbstractPresenter; +import de.wwwu.awolf.presenter.data.generator.CircleDatasetGenerator; +import de.wwwu.awolf.presenter.data.generator.CloudDatasetGenerator; +import de.wwwu.awolf.presenter.data.generator.LineDatasetGenerator; +import de.wwwu.awolf.presenter.data.io.DataExporter; +import de.wwwu.awolf.presenter.data.io.DataImporter; +import de.wwwu.awolf.presenter.util.Logging; +import java.io.File; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; + +public class DataHandler { + + private final AbstractPresenter presenter; + + public DataHandler(AbstractPresenter presenter) { + this.presenter = presenter; + } + + public Set getData(final File file) { + //Presenter soll die Klasse überwachen + ExecutorCompletionService> completionService = new ExecutorCompletionService<>( + this.presenter.getExecutor()); + Logging.logDebug("Importing Data: " + file.getAbsolutePath()); + completionService.submit(new DataImporter(file)); + //wait until future is ready + try { + return completionService.take().get(); + } catch (InterruptedException | ExecutionException e) { + Logging.logError("Interrupted while importing... ", e); + Thread.currentThread().interrupt(); + return Collections.emptySet(); + } + } + + + public Set getData(final DataType type, final int dataSize) { + //Presenter soll die Klasse überwachen + ExecutorCompletionService> completionService = new ExecutorCompletionService<>( + this.presenter.getExecutor()); + Logging.logDebug("Generating Data: Size: " + dataSize + ", dataType: " + type.name()); + + switch (type) { + case CIRCLE: + completionService.submit(new CircleDatasetGenerator(dataSize)); + break; + case LINE: + completionService.submit(new LineDatasetGenerator(dataSize)); + break; + case CLOUD: + completionService.submit(new CloudDatasetGenerator(dataSize)); + break; + default: + return Collections.emptySet(); + } + + try { + return completionService.take().get(); + } catch (InterruptedException | ExecutionException e) { + Logging.logError("Interrupted while generating... ", e); + Thread.currentThread().interrupt(); + return Collections.emptySet(); + } + } + + public void exportData(File file, Set lines) { + this.presenter.getExecutor().submit(new DataExporter(lines, file)); + } + + + public enum DataType { + CLOUD, + LINE, + CIRCLE + } + + public enum ActionType { + IMPORT, + EXPORT, + GENERATE + } + + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/data/generator/CircleDatasetGenerator.java b/src/main/java/de/wwwu/awolf/presenter/data/generator/CircleDatasetGenerator.java new file mode 100644 index 0000000..e5e28ce --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/generator/CircleDatasetGenerator.java @@ -0,0 +1,63 @@ +package de.wwwu.awolf.presenter.data.generator; + +import static de.wwwu.awolf.presenter.data.generator.DatasetGenerator.generateDataLines; + +import de.wwwu.awolf.model.Line; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 01.08.2017. + */ +public class CircleDatasetGenerator implements Callable> { + + private final int size; + + public CircleDatasetGenerator(int size) { + this.size = size; + } + + /** + * Generiert einen Datensatz des typen: Gerade mit zirkulärer Störung. Zuerst wird die zirkuläre Störung zu der Liste der (dualen-)Geraden hinzugefügt danach wird die wrapper Methode + * generateDataLines() aufgerufen. + * + * @return Liste der Geraden + */ + @Override + public Set call() throws Exception { + Set lines = new HashSet<>(); + + double from = 0; + double to = Math.PI * 5; + //obere Grenze für die neuen Punkte + int n = size / 2 + lines.size(); + + //calculate the distance between every two points + double distance = (to - from) / ((double) n); + + //create points + double currentDistance = from; + //an die aktuelle Liste dranhängen + for (int i = lines.size(); i < n; i++) { + double x = Math.cos(currentDistance); + double y = Math.sin(currentDistance); + + Line line = new Line(x, y); + line.setId(i + ""); + lines.add(line); + + //distance for the next iteration + currentDistance += distance; + } + + int generateSize = (int) (size * 0.5); + + return generateDataLines(lines, generateSize); + } +} + diff --git a/src/main/java/de/wwwu/awolf/presenter/data/generator/CloudDatasetGenerator.java b/src/main/java/de/wwwu/awolf/presenter/data/generator/CloudDatasetGenerator.java new file mode 100644 index 0000000..dbfe53d --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/generator/CloudDatasetGenerator.java @@ -0,0 +1,53 @@ +package de.wwwu.awolf.presenter.data.generator; + +import de.wwwu.awolf.model.Line; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 01.08.2017. + */ +public class CloudDatasetGenerator implements Callable> { + + private final int size; + + public CloudDatasetGenerator(int size) { + this.size = size; + } + + /** + * Generiert zu einer gegebenen Größe einen Datensatz des typen: Punktwolke + * + * @return Liste der Geraden + */ + public Set generateDataCloud() { + Set lines = new HashSet<>(); + SecureRandom random = new SecureRandom(); + double m = 1 + random.nextDouble(); + double b = random.nextDouble(); + + for (int i = 1; i < (size + 1); i++) { + double y = (random.nextGaussian() * 100) % 100; + double signal = m * i + b; + signal *= -1; + + Line line = new Line(i, signal - y); + line.setId(i - 1 + ""); + lines.add(line); + } + + return lines; + } + + @Override + public Set call() throws Exception { + return generateDataCloud(); + } +} + diff --git a/src/main/java/de/wwwu/awolf/presenter/data/generator/DatasetGenerator.java b/src/main/java/de/wwwu/awolf/presenter/data/generator/DatasetGenerator.java new file mode 100644 index 0000000..801879a --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/generator/DatasetGenerator.java @@ -0,0 +1,67 @@ +package de.wwwu.awolf.presenter.data.generator; + +import de.wwwu.awolf.model.Line; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 01.08.2017. + */ +public class DatasetGenerator { + + + /** + * Konstruktor + */ + private DatasetGenerator() { + + } + + + /** + * Generieren eines Datensatzes des typen: Gerade. Die Geraden werden in eine übergebene Liste hinzugefügt. + * + * @param n Größe des Datensatzes + * @return Liste des Geraden + */ + public static Set generateDataLines(Set lines, int n) { + + double m = 5d; + double b = 0d; + + int size = 0; + Map points = new HashMap<>(); + + //speichere die Koordinaten in einer HashMap, damit keine Punkte + //entstehen deren x-Koordinate zu sehr beieinander liegt. + SecureRandom random = new SecureRandom(); + while (size < n) { + double y = random.nextGaussian(); + double signal = m * y + b; + signal *= -1; + + if (!points.containsKey(y)) { + points.put(y, signal); + size++; + } + } + + int idx = lines.size(); + for (Map.Entry d : points.entrySet()) { + Line line = new Line(d.getKey(), d.getValue()); + line.setId(idx + ""); + lines.add(line); + idx++; + } + + return lines; + } + +} + diff --git a/src/main/java/de/wwwu/awolf/presenter/data/generator/LineDatasetGenerator.java b/src/main/java/de/wwwu/awolf/presenter/data/generator/LineDatasetGenerator.java new file mode 100644 index 0000000..2464807 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/generator/LineDatasetGenerator.java @@ -0,0 +1,35 @@ +package de.wwwu.awolf.presenter.data.generator; + +import de.wwwu.awolf.model.Line; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 01.08.2017. + */ +public class LineDatasetGenerator implements Callable> { + + private final int size; + + + public LineDatasetGenerator(int size) { + this.size = size; + } + + + /** + * Wrapper Methode zum generieren eines Datensatzes des typen: Gerade + * + * @return Liste des Geraden + */ + @Override + public Set call() throws Exception { + return DatasetGenerator.generateDataLines(new HashSet<>(), size); + } +} + diff --git a/src/main/java/de/wwwu/awolf/presenter/data/io/DataExporter.java b/src/main/java/de/wwwu/awolf/presenter/data/io/DataExporter.java new file mode 100644 index 0000000..f66328b --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/io/DataExporter.java @@ -0,0 +1,61 @@ +package de.wwwu.awolf.presenter.data.io; + +import com.opencsv.CSVWriter; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.util.Logging; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 03.08.2017. + */ +public class DataExporter implements Runnable { + + private Set lines; + private File file; + + /** + * Konstruktor + * + * @param lines Liste der Geraden + * @param file Datei in die, die Informationen exportiert werden sollen + */ + public DataExporter(Set lines, File file) { + this.file = file; + this.lines = lines; + } + + /** + * Diese Methode schreibt die Geraden der Form: y = mx + b, in eine Komma-Separierte Datei (CSV). Der Aufbau der Datei ist: id, m, b. Wenn der Export beendet wurde wird die Beobachter-Klasse + * informiert. In diesem Fall ist dies die Presenter Klasse. + */ + @Override + public void run() { + CSVWriter writer = null; + + try { + writer = new CSVWriter(new FileWriter(file)); + String[] entries = new String[3]; + for (Line line : lines) { + entries[0] = line.getId(); + entries[1] = line.getM().toString(); + double tmp = (-1) * line.getB(); + entries[2] = Double.toString(tmp); + writer.writeNext(entries); + } + writer.close(); + + } catch (IOException e) { + Logging.logError(e.getMessage(), e); + } + + Logging.logInfo( + "The model has been successfully saved under: " + file.getAbsolutePath() + "."); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/data/io/DataImporter.java b/src/main/java/de/wwwu/awolf/presenter/data/io/DataImporter.java new file mode 100644 index 0000000..a5c0bd5 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/data/io/DataImporter.java @@ -0,0 +1,88 @@ +package de.wwwu.awolf.presenter.data.io; + +import com.opencsv.CSVReader; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.util.Logging; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; + + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 21.06.2017. + */ +public class DataImporter implements Callable> { + + private CSVReader reader; + + /** + * Konstruktor + * + * @param file Datei aus der die Informationen imortiert werden sollen. + */ + public DataImporter(File file) { + + try { + this.reader = new CSVReader(new FileReader(file)); + } catch (FileNotFoundException e) { + Logging.logError(e.getMessage(), e); + } + } + + /** + * Diese Methode importiert liest zeile für zeile die Daten aus der Datei und baut eine Liste von Geraden auf. Dabei wird auf die richtige Form geachtet. Falls die Datei nicht, mindestens zwei + * Spalten enthält wird ein Fehler signalisiert und der Import wird abgebrochen. + * + * @return Liste der Geraden + */ + @Override + public Set call() throws Exception { + Set list = new HashSet<>(); + try { + List lines = reader.readAll(); + int counter = 0; + String[] result = {"import", lines.size() + "", ""}; + for (String[] nextLine : lines) { + if (nextLine.length == 3) { + double x = Double.parseDouble(nextLine[1]); + double y = Double.parseDouble(nextLine[2]) * (-1); + Line line = new Line(x, y); + line.setId(nextLine[0] + ""); + list.add(line); + counter++; + result[2] = counter + ""; + Thread.sleep(10); + } else if (nextLine.length == 2) { + double x = Double.parseDouble(nextLine[1]); + double y = Double.parseDouble(nextLine[2]) * (-1); + Line line = new Line(x, y); + line.setId(counter + ""); + list.add(line); + + counter++; + result[2] = counter + ""; + Thread.sleep(10); + } else { + Logging.logWarning( + "Diese Datei kann nicht importiert werden. Es müssen mindestens zwei Spalten enthalten sein (x,y). Fehler bei der Eingabe"); + return null; + } + } + } catch (IOException e) { + Logging.logError(e.getMessage(), e); + } catch (InterruptedException e) { + Logging.logError(e.getMessage(), e); + Thread.currentThread().interrupt(); + } + return list; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/evaluation/AlgorithmComparison.java b/src/main/java/de/wwwu/awolf/presenter/evaluation/AlgorithmComparison.java new file mode 100644 index 0000000..5381268 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/evaluation/AlgorithmComparison.java @@ -0,0 +1,48 @@ +package de.wwwu.awolf.presenter.evaluation; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.evaluation.ComparisonResult; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.algorithms.AlgorithmHandler; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AlgorithmComparison { + + private final List types; + private final ExecutorService executorService; + private final CompletionService completionService; + private final Set lines; + + public AlgorithmComparison(List types, Set lines) { + this.types = types; + this.lines = lines; + this.executorService = Executors.newFixedThreadPool(3); + completionService = new ExecutorCompletionService<>(this.executorService); + } + + public ComparisonResult compare() { + + ComparisonResult comparisonResult = new ComparisonResult(); + AlgorithmHandler algorithmHandler = AlgorithmHandler.getInstance(); + + for (Algorithm.Type value : getTypes()) { + Algorithm.Type type; + synchronized (types) { + type = value; + } + + Line line = algorithmHandler.runAlgorithmByType(type, lines); + comparisonResult.put(type, line); + } + return comparisonResult; + } + + private List getTypes() { + return types; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/evaluation/EvaluatationHandler.java b/src/main/java/de/wwwu/awolf/presenter/evaluation/EvaluatationHandler.java new file mode 100644 index 0000000..5f52508 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/evaluation/EvaluatationHandler.java @@ -0,0 +1,282 @@ +package de.wwwu.awolf.presenter.evaluation; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.LineModel; +import de.wwwu.awolf.model.communication.Data; +import de.wwwu.awolf.model.communication.EvaluationData; +import de.wwwu.awolf.model.communication.SubscriberType; +import de.wwwu.awolf.model.evaluation.ComparisonResult; +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.data.DataHandler; +import de.wwwu.awolf.presenter.data.generator.DatasetGenerator; +import de.wwwu.awolf.presenter.evaluation.measures.PercentageErrorBasedMeasure; +import de.wwwu.awolf.presenter.evaluation.measures.ScaleDependentMeasure; +import de.wwwu.awolf.presenter.evaluation.measures.ScaledErrorBasedMeasure; +import de.wwwu.awolf.presenter.util.Logging; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Flow; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 01.08.2017. + */ +public class EvaluatationHandler implements Runnable, Flow.Publisher { + + private LineModel arrangement; + private DatasetGenerator generator; + + //übergebene Parameter + private int type; + private int iterations; + private int alg; + private Flow.Subscriber subscriber; + private Map> resultMapping; + + /** + * Konstruktor zur evaluation + * + * @param type Typ der evaluation + * @param n Größe des Datensatzes + * @param alg 0 = lms, 1 = rm, 2 = ts, 3 = lms, rm, 4 = lms, ts, 5 = rm, ts, 6 = lms, rm, ts, + * @param datasettyp typ der zu generierenden Datensatz + */ + public EvaluatationHandler(int type, int n, int alg, DataHandler.DataType datasettyp) { + Presenter instance = Presenter.getInstance(); + subscribe(instance); + this.arrangement = new LineModel(); + this.resultMapping = new EnumMap<>(Algorithm.Type.class); + + Set data = instance.generateDataset(n, datasettyp); + Logging.logInfo("Starting the Benchmark..."); + arrangement.setLines(data); + + Logging.logInfo("Benchmark on Dataset: " + datasettyp + " with " + n + " points"); + + this.type = type; + this.iterations = n; + this.alg = alg; + } + + /** + * Konstruktor zur evaluation + * + * @param type Typ der evaluation + * @param alg 0 = lms, 1 = rm, 2 = ts, 3 = lms, rm, 4 = lms, ts, 5 = rm, ts, 6 = lms, rm, ts, + * @param file Datei die importiert werden soll + */ + public EvaluatationHandler(int type, int alg, File file) { + Presenter instance = Presenter.getInstance(); + subscribe(instance); + instance.importDataset(file); + this.arrangement = instance.getModel(); + this.resultMapping = new EnumMap<>(Algorithm.Type.class); + + this.type = type; + this.alg = alg; + } + + private Map> benchmarkSameEstimator( + final Algorithm.Type advanced, final Algorithm.Type naiv) { + Logging + .logInfo("AlgorithmComparison with Types: " + advanced.name() + ", " + naiv.name()); + AlgorithmComparison comparison = new AlgorithmComparison(Arrays.asList(naiv, advanced), + Collections.unmodifiableSet(arrangement.getLines())); + ComparisonResult comparisonResult = comparison.compare(); + + Map result = new HashMap<>(); + result.putAll( + getScaleDependentMeasure(arrangement.getLines(), comparisonResult, advanced)); + result.putAll( + getScaledErrorBasedMeasure(arrangement.getLines(), comparisonResult, advanced, + naiv)); + Logging.logInfo("finished with execution of the algorithms."); + + this.resultMapping.put(advanced, result); + return this.resultMapping; + } + + private Map> benchmarkDifferentEstimators( + List types) { + Logging.logInfo("AlgorithmComparison with Types: " + types); + AlgorithmComparison comparison = new AlgorithmComparison(types, + Collections.unmodifiableSet(arrangement.getLines())); + ComparisonResult comparisonResult = comparison.compare(); + + types.forEach(typeEntry -> this.resultMapping.put(typeEntry, + getPercentigeErrorBasedMeasure(arrangement.getLines(), comparisonResult, + typeEntry))); + Logging.logInfo("finished with execution of the algorithms."); + return this.resultMapping; + } + + + /** + * Startet die Evaluation zu den passenden Typ. Bei beendigung wird der Beobachter informiert. + */ + @Override + public void run() { + Map> result = new EnumMap<>(Algorithm.Type.class); + switch (type) { + case 0: + //der alg der gewählt wurde + if (alg == 0) { + result = benchmarkSameEstimator(Algorithm.Type.LMS, + Algorithm.Type.NAIVE_LMS); + } else if (alg == 1) { + result = benchmarkSameEstimator(Algorithm.Type.RM, + Algorithm.Type.NAIVE_RM); + } else { + result = benchmarkSameEstimator(Algorithm.Type.TS, + Algorithm.Type.NAIVE_TS); + } + break; + case 1: + switch (alg) { + case 3: + result = benchmarkDifferentEstimators( + Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.RM)); + break; + case 4: + result = benchmarkDifferentEstimators( + Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.TS)); + break; + case 5: + result = benchmarkDifferentEstimators( + Arrays.asList(Algorithm.Type.RM, Algorithm.Type.TS)); + break; + case 6: + result = benchmarkDifferentEstimators( + Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.RM, + Algorithm.Type.TS)); + break; + } + break; + } + sendTableApproximationData(result); + } + + /** + * Die berechneten Ergebnisse werden an den Beobachter übermittelt um dann visualisiert zu werden. + * + * @param result Ergebnisse + */ + private void sendTableApproximationData(Map> result) { + + EvaluationData data = new EvaluationData(); + data.setType(SubscriberType.EVALUATION_TABLE_DATA); + data.setMultipleColumnResult(result); + data.setRowsPerColumn(result.keySet().size()); + this.subscriber.onNext(data); + } + + /** + * Startet die Berechnung der skalierungsabbhängigen Maße. + * + * @param lines Liste der Geraden + * @return Liste mit den Ergebnissen, bereit zum visualisieren + */ + private Map getScaleDependentMeasure(final Set lines, + final ComparisonResult comparisonResult, final Algorithm.Type type) { + + Logging.logInfo("Calculating ScaleDependentMeasure for " + type); + + Double m = comparisonResult.get(type).getM(); + Double b = comparisonResult.get(type).getB(); + + ScaleDependentMeasure scaleDependentMeasure = new ScaleDependentMeasure(lines, m, b); + Map ret = new HashMap<>(); + ret.put(type + " MSE", scaleDependentMeasure.mse().toString()); + ret.put(type + " RMSE", scaleDependentMeasure.rmse().toString()); + ret.put(type + " MAE", scaleDependentMeasure.mae().toString()); + ret.put(type + " MDAE", scaleDependentMeasure.mdae().toString()); + ret.put(type + " SLOPE", m.toString()); + ret.put(type + " y-INTERCEPTION", b.toString()); + + Logging.logInfo("finished calculating ScaleDependentMeasure."); + + return ret; + } + + /** + * Startet die Berechnung der Maße die auf dem prozentualen Fehler basieren. + * + * @param lines Liste der Geraden + * @return Liste mit den Ergebnissen, bereit zum visualisieren + */ + private Map getPercentigeErrorBasedMeasure(final Set lines, + final ComparisonResult comparisonResult, final Algorithm.Type type) { + Logging.logInfo("Calculating PercentigeErrorBasedMeasure for " + type); + Double m = comparisonResult.get(type).getM(); + Double b = comparisonResult.get(type).getB(); + + PercentageErrorBasedMeasure percentageErrorBasedMeasure = new PercentageErrorBasedMeasure( + lines, + m, b); + Map ret = new HashMap<>(); + ret.put(type + " MAPE", percentageErrorBasedMeasure.mape().toString()); + ret.put(type + " MDAPE", percentageErrorBasedMeasure.mdape().toString()); + ret.put(type + " RMSPE", percentageErrorBasedMeasure.rmspe().toString()); + ret.put(type + " RMDSPE", percentageErrorBasedMeasure.rmdspe().toString()); + ret.put(type + " SLOPE", m.toString()); + ret.put(type + " y-INTERCEPTION", b.toString()); + Logging.logInfo("finished calculating PercentigeErrorBasedMeasure."); + return ret; + } + + /** + * Startet die Berechnung der skalierungsunabbhängigen Maße. + * + * @param lines Liste der Geraden + * @return Liste mit den Ergebnissen, bereit zum visualisieren + */ + private Map getScaledErrorBasedMeasure(final Set lines, + final ComparisonResult comparisonResult, final Algorithm.Type advanced, + final Algorithm.Type naiv) { + + Logging.logInfo("Calculating ScaledErrorBasedMeasure for " + advanced + ", " + naiv); + + //first + Double m = comparisonResult.get(advanced).getM(); + Double b = comparisonResult.get(advanced).getB(); + //second + Double naivM = comparisonResult.get(naiv).getM(); + Double naivB = comparisonResult.get(naiv).getB(); + + ScaledErrorBasedMeasure scaledErrorBasedMeasure = new ScaledErrorBasedMeasure(lines, m, + b, + naivM, naivB); + Map ret = new HashMap<>(); + ret.put(advanced + " MSE", scaledErrorBasedMeasure.mse().toString()); + ret.put(advanced + " RMSE", scaledErrorBasedMeasure.rmse().toString()); + ret.put(advanced + " MAE", scaledErrorBasedMeasure.mae().toString()); + ret.put(advanced + " MDAE", scaledErrorBasedMeasure.mdae().toString()); + ret.put(advanced + " Naiv-SLOPE", naivM.toString()); + ret.put(advanced + " Naiv-y-INTERCEPTION", naivB.toString()); + + Logging.logInfo("finished calculating ScaledErrorBasedMeasure."); + return ret; + } + + /** + * @return Liste der Geraden auf der die Berechnungen ausgeführt wurden + */ + public Set getData() { + return arrangement.getLines(); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/PercentageErrorBasedMeasure.java b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/PercentageErrorBasedMeasure.java new file mode 100644 index 0000000..349adc1 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/PercentageErrorBasedMeasure.java @@ -0,0 +1,111 @@ +package de.wwwu.awolf.presenter.evaluation.measures; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 07.09.2017. + */ +public class PercentageErrorBasedMeasure { + + + private List percentageError; + + /** + * Konstruktor + * + * @param lines Liste der Geraden + * @param m Steigung + * @param b y-Achenabschnitt + */ + public PercentageErrorBasedMeasure(final Set lines, Double m, Double b) { + + //Berechnung des Sampson-Fehlers + List linesAsList = new ArrayList<>(lines); + List sampson = new ArrayList<>(); + for (Line line : linesAsList) { + Double e = Math.pow(m * line.getM() - line.getB() + b, 2) / (Math.pow(m, 2) + 1); + sampson.add(e); + } + + percentageError = new ArrayList<>(); + //Berechnung der prozentuelen-Fehlers + for (int j = 0; j < sampson.size(); j++) { + percentageError.add(100 * sampson.get(j) / linesAsList.get(j).getB()); + } + } + + /* Percentege Error Approximation Measures */ + //verschiedene Eingaben für einen Alg. + + /** + * Mean Absolute Precentage Error + * + * @return Ergebnis + */ + public Double mape() { + double error = 0; + + for (Double d : percentageError) { + error += Math.abs(d); + } + + error /= percentageError.size(); + + return error; + } + + /** + * Median Absolute Precentage Error: + * + * @return Ergebnis + */ + public Double mdape() { + + ArrayList abs = new ArrayList<>(); + + for (Double d : percentageError) { + abs.add(Math.abs(d)); + } + + return FastElementSelector.randomizedSelect(abs, abs.size() * 0.5); + } + + /** + * Root Mean Square Percentage Error + * + * @return Ergebnis + */ + public Double rmspe() { + double error = 0; + + for (Double d : percentageError) { + error += Math.pow(d, 2); + } + + error /= percentageError.size(); + + return Math.sqrt(error); + } + + /** + * Root Median Square Percentage Error + * + * @return Ergebnis + */ + public Double rmdspe() { + List squares = new ArrayList<>(); + for (Double d : percentageError) { + squares.add(Math.pow(d, 2)); + } + + return Math.sqrt(FastElementSelector.randomizedSelect(squares, squares.size() * 0.5)); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaleDependentMeasure.java b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaleDependentMeasure.java new file mode 100644 index 0000000..89b677a --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaleDependentMeasure.java @@ -0,0 +1,90 @@ +package de.wwwu.awolf.presenter.evaluation.measures; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 07.09.2017. + */ +public class ScaleDependentMeasure { + + private Set errorValues; + + /** + * Konstruktor + * + * @param lines Liste des Geraden + * @param m Steigung + * @param b y-Achsenabschnitt + */ + public ScaleDependentMeasure(final Set lines, Double m, Double b) { + //Liste mit den Fehler zu jedem Punkt + errorValues = new HashSet<>(); + + //Sampson-Fehler Berechnung + for (Line line : lines) { + Double e = Math.pow(m * line.getM() - line.getB() + b, 2) / (Math.pow(m, 2) + 1); + errorValues.add(e); + } + } + + /* Skalierungs Abhängige Approximationsgüten */ + //unterschiedliche Alg.- auf einem Datensatz + + /** + * Mean Square Error: + * + * @return Ergebnis + */ + public Double mse() { + double error = 0; + + for (Double d : errorValues) { + error += Math.pow(d, 2); + } + + error /= errorValues.size(); + + return error; + } + + /** + * Root Mean Square Error + * + * @return Ergebnis + */ + public Double rmse() { + return Math.sqrt(mse()); + } + + /** + * Mean Absolute Error + * + * @return Ergebnis + */ + public Double mae() { + double error = 0; + for (Double d : errorValues) { + error += Math.abs(d); + } + error /= errorValues.size(); + return error; + } + + /** + * Median Absolute Error + * + * @return Ergebnis + */ + public Double mdae() { + return FastElementSelector + .randomizedSelect(new ArrayList<>(errorValues), errorValues.size() * 0.5); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaledErrorBasedMeasure.java b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaledErrorBasedMeasure.java new file mode 100644 index 0000000..7b3f6e3 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/evaluation/measures/ScaledErrorBasedMeasure.java @@ -0,0 +1,108 @@ +package de.wwwu.awolf.presenter.evaluation.measures; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.util.FastElementSelector; +import java.util.ArrayList; +import java.util.Set; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 07.09.2017. + */ +public class ScaledErrorBasedMeasure { + + + private ArrayList sampsonError; + private ArrayList naivSampsonError; + private ArrayList scaledError; + + /** + * Konstruktor + * + * @param lines Liste der Geraden + * @param m Steigung + * @param b y-Achsenabschnitt + * @param naivSlope naive Steigung + * @param naivInterception naiver y-Achsenabschnitt + */ + public ScaledErrorBasedMeasure(final Set lines, Double m, Double b, Double naivSlope, + Double naivInterception) { + + this.sampsonError = new ArrayList<>(); + this.naivSampsonError = new ArrayList<>(); + this.scaledError = new ArrayList<>(); + + //Sampson-Fehler der naiven Mehtode + for (Line line : lines) { + Double e = + Math.pow(naivSlope * line.getM() - line.getB() + naivInterception, 2) / ( + Math.pow(naivSlope, 2) + 1); + naivSampsonError.add(e); + } + + //Sampson-Fehler der fortgeschrittenen Methode + for (Line line : lines) { + Double e = Math.pow(m * line.getM() - line.getB() + b, 2) / (Math.pow(m, 2) + 1); + sampsonError.add(e); + } + + //skalierungs-Fehler + for (int i = 0; i < sampsonError.size(); i++) { + scaledError.add(sampsonError.get(i) / naivSampsonError.get(i)); + } + } + + /* Skalierungs Abhängige Approximationsgüten */ + //unterschiedliche Alg.- auf einem Datensatz + + /** + * Mean Square Error + * + * @return Ergebnis + */ + public Double mse() { + double error = 0; + for (Double d : scaledError) { + error += Math.pow(d, 2); + } + error /= scaledError.size(); + return error; + } + + /** + * Root Mean Square Error + * + * @return Ergebnis + */ + public Double rmse() { + return Math.sqrt(mse()); + } + + /** + * Mean Absolute Error: + * + * @return Ergebnis + */ + public Double mae() { + double error = 0; + for (Double d : scaledError) { + error += Math.abs(d); + } + error /= scaledError.size(); + return error; + } + + /** + * Median Absolute Error + * + * @return Ergebnis + */ + public Double mdae() { + return FastElementSelector + .randomizedSelect(scaledError, scaledError.size() * 0.5); + } + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/BinomialCoeffizient.java b/src/main/java/de/wwwu/awolf/presenter/util/BinomialCoeffizient.java new file mode 100644 index 0000000..3fdfa83 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/BinomialCoeffizient.java @@ -0,0 +1,33 @@ +package de.wwwu.awolf.presenter.util; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 26.06.2017. + */ +public class BinomialCoeffizient { + + /** + * Berechnet den Binomialkoeffizient zu der eingabe. Bin(n,k) + * + * @param n n + * @param k k + * @return Ergebnis + */ + public static Double run(int n, int k) { + int res = 1; + + if (k > n - k) { + k = n - k; + } + + for (int i = 0; i < k; ++i) { + res *= (n - i); + res /= (i + 1); + } + + return Double.valueOf(res); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorBegin.java b/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorBegin.java new file mode 100644 index 0000000..aaf7b44 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorBegin.java @@ -0,0 +1,19 @@ +package de.wwwu.awolf.presenter.util.Comparators; + +import de.wwwu.awolf.model.Line; +import java.util.Comparator; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 19.06.2017. + */ +public class YOrderLineComparatorBegin implements Comparator { + + @Override + public int compare(Line o1, Line o2) { + return o1.getY1().compareTo(o2.getY1()); + } +} \ No newline at end of file diff --git a/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorEnd.java b/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorEnd.java new file mode 100644 index 0000000..fc2b94f --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/Comparators/YOrderLineComparatorEnd.java @@ -0,0 +1,19 @@ +package de.wwwu.awolf.presenter.util.Comparators; + +import de.wwwu.awolf.model.Line; +import java.util.Comparator; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 19.06.2017. + */ +public class YOrderLineComparatorEnd implements Comparator { + + @Override + public int compare(Line o1, Line o2) { + return o1.getY2().compareTo(o2.getY2()); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/FastElementSelector.java b/src/main/java/de/wwwu/awolf/presenter/util/FastElementSelector.java new file mode 100644 index 0000000..0110aeb --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/FastElementSelector.java @@ -0,0 +1,95 @@ +package de.wwwu.awolf.presenter.util; + +import java.security.SecureRandom; +import java.util.Collections; +import java.util.List; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 29.06.2017. + */ +public class FastElementSelector { + + /** + * Liefert das k-te Element aus der Eingabeliste zurück in Theta(n) Zeit. + * + * @param a Eingabeliste + * @param i Rang des gewünschten Elements + * @return das Element + */ + public static Double randomizedSelect(List a, double i) { + + int start = 0; + int end = a.size() - 1; + + if (i >= end + 1) { + return a.get(end); + } + + while (true) { + if (start >= end) { + return a.get(start); + } + int q = randomizedPartition(a, start, end); + int k = q - start + 1; + + if (i == k) { + return a.get(q); + } else { + if (i < k) { + end = q - 1; + } else { + start = q + 1; + i = i - k; + } + } + } + } + + /** + * Hilfsmethode + * + * @param a Eingabeliste + * @param start Startindex + * @param end Index des letzten Elements + * @return Pivotelement + */ + private static int randomizedPartition(List a, int start, int end) { + int i = 0; + SecureRandom random = new SecureRandom(); + + //alternative: ThreadLocalRandom.current() + if (start < end) { + i = start + random.nextInt(end - start); + } else { + i = end + random.nextInt(start - end); + } + + Collections.swap(a, end, i); + return partition(a, start, end); + } + + /** + * Hilfsmethode + * + * @param a Eingabeliste + * @param start Startindex + * @param end Index des letzten Elements + * @return Pivotelement + */ + private static int partition(List a, int start, int end) { + Double x = a.get(end); + int i = start - 1; + for (int j = start; j <= end - 1; j++) { + if (a.get(j) <= x) { + i++; + Collections.swap(a, i, j); + } + } + Collections.swap(a, i + 1, end); + return i + 1; + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/IntersectionComputer.java b/src/main/java/de/wwwu/awolf/presenter/util/IntersectionComputer.java new file mode 100644 index 0000000..686108a --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/IntersectionComputer.java @@ -0,0 +1,173 @@ +package de.wwwu.awolf.presenter.util; + +import com.google.common.collect.Lists; +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.Point; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.RecursiveTask; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 18.09.2017. + */ +public class IntersectionComputer { + + private static IntersectionComputer instance; + + /** + * Konstruktor + */ + private IntersectionComputer() { + } + + public static IntersectionComputer getInstance() { + if (instance == null) { + instance = new IntersectionComputer(); + Logging.logInfo("Created instance of IntersectionComputer"); + } + + return instance; + } + + /** + * Berechnet zu einer gegebenen Menge von dualen Geraden die Schnittpunkte. Dafür wird ein modifizierter Merge-Sort Algorithmus verwendett. Um die Performance zu steigern wird die Berechnung ab + * einer passenden Größe auf vier Threads ausgelagert. + * + * @param lower untere Schranke + * @param higher obere Schranke + * @return Liste der Schnittpunkte + */ + public Set compute(final Collection lines, final double lower, + final double higher) { + + final Set fullInput = new HashSet<>(lines); + final Set copyInput = new HashSet<>(lines); + + if (lower == higher) { + return Collections.emptySet(); + } else { + Logging.logDebug( + "Open ForkJoinPool: lines: " + lines.size() + " I(" + lower + ", " + higher + + "]"); + ForkJoinPool pool = ForkJoinPool.commonPool(); + RecursiveComputationTask recursiveComputationTask = new RecursiveComputationTask( + fullInput, + copyInput, lower, higher); + pool.execute(recursiveComputationTask); + Set join = recursiveComputationTask.join(); + return join; + } + } + + /** + * Berechnet die Schnittpunkte zwischen einer gegebenen Gerade und einer Menge an Geraden. + * + * @param set Menge an Geraden + * @param sampledLine eine spezielle Gerade + * @return Liste mit x Koordinaten der Schnittpunkte + */ + public List calculateIntersectionAbscissas(Collection set, Line sampledLine, + double lower, double upper) { + List lines = new LinkedList<>(set); + Set intersections = new HashSet<>(); + + for (Line line : lines) { + if (line != sampledLine) { + if (sampledLine.doIntersect(line, lower, upper)) { + intersections.add(sampledLine.intersect(line).getX()); + } + } + } + + return new ArrayList<>(intersections); + } + + private class RecursiveComputationTask extends RecursiveTask> { + + private static final int THRESHOLD = 200; + + private final Set lines; + private final Set fullList; + private final double lower; + private final double upper; + + public RecursiveComputationTask(final Set fullList, final Set lines, + final double lower, final double upper) { + this.lines = lines; + this.fullList = fullList; + this.lower = lower; + this.upper = upper; + } + + @Override + protected Set compute() { + if (this.lines.isEmpty()) { + return Collections.emptySet(); + } else if (this.lines.size() > THRESHOLD) { + return ForkJoinTask.invokeAll(createSubTask()).stream() + .map(ForkJoinTask::join) + .flatMap(Collection::stream).collect(Collectors.toSet()); + } else { + return work(this.fullList, this.lines, this.lower, this.upper); + } + } + + private Collection createSubTask() { + List dividedTasks = new ArrayList<>(); + + long midpoint = Math.round(this.lines.size() * 0.5); + + Set firstSubSet = new HashSet<>(); + Set secondSubSet = new HashSet<>(); + AtomicInteger count = new AtomicInteger(); + this.lines.forEach(next -> { + int index = count.getAndIncrement(); + if (index < midpoint) { + firstSubSet.add(next); + } else { + secondSubSet.add(next); + } + }); + + dividedTasks + .add(new RecursiveComputationTask(this.fullList, firstSubSet, this.lower, + this.upper)); + dividedTasks + .add(new RecursiveComputationTask(this.fullList, secondSubSet, this.lower, + this.upper)); + return dividedTasks; + } + + private Set work(Set fullList, Set lines, double lower, + double higher) { + Set points = new HashSet<>(); + List> lists = Lists + .cartesianProduct(new ArrayList<>(fullList), new ArrayList<>(lines)); + lists.forEach(entry -> { + if (entry.get(0).doIntersect(entry.get(1), lower, upper)) { + Point intersect = entry.get(0).intersect(entry.get(1)); + if (intersect.getX() > lower && intersect.getX() <= higher) { + points.add(intersect); + } + } + }); + return new HashSet<>(points); + } + + + } + +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/Logging.java b/src/main/java/de/wwwu/awolf/presenter/util/Logging.java new file mode 100644 index 0000000..a6e4eb8 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/Logging.java @@ -0,0 +1,50 @@ +package de.wwwu.awolf.presenter.util; + + +import org.apache.log4j.Logger; + +public class Logging { + + private static Logger logger = Logger.getRootLogger(); + + private Logging() { + + } + + + private static Logger getLogger() { + return logger; + } + + public static void logInfo(final String message, Throwable throwable) { + logger.info(message, throwable); + } + + public static void logInfo(final String message) { + logger.info(message); + } + + public static void logDebug(final String message, Throwable throwable) { + logger.debug(message, throwable); + } + + public static void logDebug(final String message) { + logger.debug(message); + } + + public static void logWarning(final String message, Throwable throwable) { + logger.warn(message, throwable); + } + + public static void logWarning(final String message) { + logger.warn(message); + } + + public static void logError(final String message, Throwable throwable) { + logger.error(message, throwable); + } + + public static void logError(final String message) { + logger.error(message); + } +} diff --git a/src/main/java/de/wwwu/awolf/presenter/util/RandomSampler.java b/src/main/java/de/wwwu/awolf/presenter/util/RandomSampler.java new file mode 100644 index 0000000..2c6a3b0 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/presenter/util/RandomSampler.java @@ -0,0 +1,44 @@ +package de.wwwu.awolf.presenter.util; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 26.06.2017. + */ +public class RandomSampler { + + private static SecureRandom random = new SecureRandom(); // Compliant for security-sensitive use cases + + private RandomSampler() { + super(); + } + + /** + * Diese Methode liefert eine r Elementige zufällige Stichprobe an Geraden. + * + * @param set Die gesammtmenge der Geraden aus denen gewählt werden soll + * @param r Anzahl der zu wählenden Geraden + * @return r Elementige zufällige Stichprobe an Geraden + */ + public static List run(List set, Double r) { + if (set.isEmpty()) { + return Collections.emptyList(); + } else { + int index = 0; + List sampled = new ArrayList<>(); + for (int i = 0; i < r; i++) { + index = random.nextInt(set.size()); + sampled.add(set.get(index)); + } + return sampled; + } + } + +} diff --git a/src/main/java/de/wwwu/awolf/view/ViewController.java b/src/main/java/de/wwwu/awolf/view/ViewController.java new file mode 100644 index 0000000..84c8913 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/view/ViewController.java @@ -0,0 +1,213 @@ +package de.wwwu.awolf.view; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.data.DataHandler; +import de.wwwu.awolf.presenter.util.Logging; +import de.wwwu.awolf.view.controller.AlgorithmTabController; +import de.wwwu.awolf.view.services.DataService; +import java.io.File; +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.stage.FileChooser; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 28.05.2017. + */ +public class ViewController { + + private static final String TITLE = "Beta JavaFX"; + private Parent pane; + + @FXML + private TabPane tabPane; + @FXML + private MenuBar menuBar; + private Map algorithmTabControllers; + + public ViewController() { + super(); + this.algorithmTabControllers = new EnumMap<>(Algorithm.Type.class); + } + + public Map getAlgorithmTabControllers() { + return algorithmTabControllers; + } + + public void initGui() { + initMenuBar(); + initTabbedPane(); + } + + private void initTabbedPane() { + //TODO + Map props = new HashMap<>(); + props.put("sfsdf", "dsadasd"); + props.put("dfd", "dsadasd"); + props.put("sdfdsfg", "dsadasd"); + props.put("dsfsdfsdf", "dsadasd"); + + try { + for (Algorithm.Type value : Algorithm.Type.values()) { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation( + AlgorithmTabController.class.getResource("/views/AlgorithmTab.fxml")); + Parent parent = loader.load(); + AlgorithmTabController controller = loader.getController(); + controller.setAlgorithmType(value); + controller.setProperties(props); + controller.setModel(Presenter.getInstance().getModel()); + controller.init(); + this.algorithmTabControllers.put(value, controller); + Tab tab = new Tab(value.getLabel(), parent); + tab.setId(value.name()); + tabPane.getTabs().add(tab); + } + + } catch (IOException e) { + Logging.logError("Error while reading AlgorithmTab.fxml", e); + } + + + } + + private void initMenuBar() { + // Create menus + Menu fileMenu = new Menu("File"); + Menu toolsMenu = new Menu("Tools"); + Menu helpMenu = new Menu("Help"); + + // Create MenuItems + //export the data + MenuItem exportItem = new MenuItem("Export"); + exportItem.setOnAction(actionEvent -> { + FileChooser fileChooser = new FileChooser(); + File file = fileChooser.showOpenDialog(null); + if (file != null && file.canWrite() && file.canRead()) { + new DataService(DataHandler.ActionType.EXPORT, file).start(); + } + }); + + //start an import of data + MenuItem importItem = new MenuItem("Import"); + importItem.setOnAction(actionEvent -> { + FileChooser fileChooser = new FileChooser(); + File file = fileChooser.showOpenDialog(null); + if (file != null && file.canWrite() && file.canRead()) { + new DataService(DataHandler.ActionType.IMPORT, file).start(); + } + }); + + //exit menu item should stop the application + MenuItem exitItem = new MenuItem("Exit"); + exitItem.setOnAction(actionEvent -> { + Platform.exit(); + System.exit(0); + }); + + // about the application + MenuItem aboutItem = new MenuItem("About"); + + MenuItem generationItem = new MenuItem("Generate"); + generationItem.setOnAction(actionEvent -> { + new DataService(DataHandler.ActionType.GENERATE, DataHandler.DataType.LINE, 1000) + .start(); + }); + MenuItem evaluationItem = new MenuItem("Evaluate"); + + // Add menuItems to the Menus + fileMenu.getItems().addAll(importItem, exportItem, exitItem); + toolsMenu.getItems().addAll(generationItem, evaluationItem); + helpMenu.getItems().addAll(aboutItem); + + // Add Menus to the MenuBar + menuBar.getMenus().addAll(fileMenu, toolsMenu, helpMenu); + + } + + /******************************************************************************************************************* + * visualisierungs methoden + ******************************************************************************************************************/ + + + /** + * Visualisiert das Ergebnis des LMS-Schätzers + * + * @param line Line + */ + public void visualizeLMS(final Line line) { +// plotLMS = new PlotPanel(); +// lmsPanel.setPlotPanel(plotLMS); +// createPlot(line.getM(), line.getB(), plotLMS, lmsPanel, "LMS"); + } + + /** + * Visualisiert das Ergebnis des RM-Schätzers + * + * @param line Line + */ + public void visualizeRM(final Line line) { +// plotRM = new PlotPanel(); +// rmPanel.setPlotPanel(plotRM); +// createPlot(line.getM(), line.getB(), plotRM, rmPanel, "RM"); + } + + + /** + * Visualisiert das Ergebnis des TS-Schätzers + * + * @param line Line + */ + public void visualizeTS(final Line line) { +// plotTS = new PlotPanel(); +// tsPanel.setPlotPanel(plotTS); +// createPlot(line.getM(), line.getB(), plotTS, tsPanel, "TS"); + } + + /** + * Fügt der Evaluations-Tabelle eine Zeile mit Daten hinzu + * + * @param tableEntries Data of the Table + */ + public void appendEvalResult(Map> tableEntries) { + + } + + /** + * Fügt der Evaluations-Tabelle eine Zeile mit Daten hinzu + * + * @param res Daten der Spalte + */ + public void appendEvalResults(Map> res) { + appendEvalResult(res); + } + + /** + * Funktionalitäten werden aktiviert. + */ + public void enableFunctionality() { + } + + /** + * Funktionalitäten werden deaktiviert. + */ + public void disableFunctionality() { + } + +} diff --git a/src/main/java/de/wwwu/awolf/view/controller/AlgorithmTabController.java b/src/main/java/de/wwwu/awolf/view/controller/AlgorithmTabController.java new file mode 100644 index 0000000..1985a09 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/view/controller/AlgorithmTabController.java @@ -0,0 +1,119 @@ +package de.wwwu.awolf.view.controller; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.LineModel; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.view.services.ButtonClickService; +import java.util.Map; +import javafx.beans.property.BooleanProperty; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; + +public class AlgorithmTabController { + + private final XYChart.Series dataSerie; + private final XYChart.Series lineSerie; + @FXML + public LineChart chart; + @FXML + public VBox vBox; + @FXML + public Button startButton; + private Algorithm.Type algorithmType; + private Map properties; + private LineModel model; + + public AlgorithmTabController() { + dataSerie = new XYChart.Series<>(); + dataSerie.setName("Datapoints"); + lineSerie = new XYChart.Series<>(); + lineSerie.setName("Estimated Line"); + } + + public LineModel getModel() { + return model; + } + + public void setModel(LineModel model) { + this.model = model; + } + + public void init() { + // add GUI elements to maintain the parameters + properties.forEach((key, value) -> { + //Pane component for the layout + BorderPane borderPane = new BorderPane(); + borderPane.setPadding(new Insets(10, 0, 10, 0)); + + //text field for the input + TextField textField = new TextField(value); + + //label + Label label = new Label(key); + label.setAlignment(Pos.BASELINE_LEFT); + label.setLabelFor(textField); + label.setPadding(new Insets(2, 10, 0, 0)); + + //add components + borderPane.setLeft(label); + borderPane.setRight(textField); + vBox.getChildren().add(borderPane); + }); + + BooleanProperty booleanProperty = startButton.disableProperty(); + startButton.setOnAction(event -> { + ButtonClickService buttonClickService = new ButtonClickService(algorithmType, + booleanProperty); + buttonClickService.start(); + } + ); + + updatePlot(this.model, null); + chart.getData().add(dataSerie); + chart.getData().add(lineSerie); + } + + public void updatePlot(final LineModel model, final Line pLine) { + dataSerie.getData().clear(); + model.getLines() + .forEach( + line -> dataSerie.getData().add(new XYChart.Data<>(line.getM(), line.getB()))); + + if (pLine != null) { + lineSerie.getData().clear(); + Double x1 = pLine.calculateX1(model.getMin()); + Double y1 = pLine.calculateY1(model.getMin()); + Double x2 = pLine.calculateX2(model.getMax()); + Double y2 = pLine.calculateY2(model.getMax()); + + lineSerie.getData().add(new XYChart.Data<>(x1, y1)); + lineSerie.getData().add(new XYChart.Data<>(x2, y2)); + } + } + + public Algorithm.Type getAlgorithmType() { + return algorithmType; + } + + public void setAlgorithmType(Algorithm.Type algorithmType) { + this.algorithmType = algorithmType; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + +} diff --git a/src/main/java/de/wwwu/awolf/view/services/ButtonClickService.java b/src/main/java/de/wwwu/awolf/view/services/ButtonClickService.java new file mode 100644 index 0000000..eb55b1d --- /dev/null +++ b/src/main/java/de/wwwu/awolf/view/services/ButtonClickService.java @@ -0,0 +1,39 @@ +package de.wwwu.awolf.view.services; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import javafx.beans.property.BooleanProperty; +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +public class ButtonClickService extends Service { + + private final BooleanProperty startButtonDisabled; + private Algorithm.Type type; + + public ButtonClickService(Algorithm.Type algorithmType, BooleanProperty startButtonDisabled) { + this.startButtonDisabled = startButtonDisabled; + this.type = algorithmType; + } + + @Override + protected Task createTask() { + + return new Task() { + @Override + protected Boolean call() throws Exception { + try { + Line line = Presenter.getInstance() + .executeAlgorithmByType(type, startButtonDisabled); + if (line != null) { + return true; + } + } catch (IllegalArgumentException e) { + return true; + } + return false; + } + }; + } +} diff --git a/src/main/java/de/wwwu/awolf/view/services/DataService.java b/src/main/java/de/wwwu/awolf/view/services/DataService.java new file mode 100644 index 0000000..c6fe277 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/view/services/DataService.java @@ -0,0 +1,51 @@ +package de.wwwu.awolf.view.services; + +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.presenter.data.DataHandler; +import java.io.File; +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +public class DataService extends Service { + + private File file; + private DataHandler.DataType type; + private DataHandler.ActionType actionType; + private int n; + + public DataService(final DataHandler.ActionType actionType, final DataHandler.DataType type, + final int n) { + this.type = type; + this.actionType = actionType; + this.n = n; + } + + public DataService(final DataHandler.ActionType actionType, final File file) { + this.actionType = actionType; + this.file = file; + this.type = null; + this.n = -1; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Boolean call() throws Exception { + switch (actionType) { + case EXPORT: + Presenter.getInstance().exportDataset(file); + break; + case IMPORT: + Presenter.getInstance().importDataset(file); + break; + case GENERATE: + Presenter.getInstance().generateDataset(n, type); + break; + } + + return true; + } + }; + } +} diff --git a/src/main/java/de/wwwu/awolf/view/services/GuiRegisterService.java b/src/main/java/de/wwwu/awolf/view/services/GuiRegisterService.java new file mode 100644 index 0000000..a64bc03 --- /dev/null +++ b/src/main/java/de/wwwu/awolf/view/services/GuiRegisterService.java @@ -0,0 +1,28 @@ +package de.wwwu.awolf.view.services; + +import de.wwwu.awolf.presenter.Presenter; +import de.wwwu.awolf.view.ViewController; +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +public class GuiRegisterService extends Service { + + private final ViewController view; + + + public GuiRegisterService(final ViewController view) { + this.view = view; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Boolean call() throws Exception { + Presenter.getInstance().registerView(view); + return true; + } + }; + } + +} diff --git a/src/main/resources/META-INF/services/de.wwwu.awolf.presenter.algorithms.Algorithm b/src/main/resources/META-INF/services/de.wwwu.awolf.presenter.algorithms.Algorithm new file mode 100644 index 0000000..0ac6867 --- /dev/null +++ b/src/main/resources/META-INF/services/de.wwwu.awolf.presenter.algorithms.Algorithm @@ -0,0 +1,6 @@ +de.wwwu.awolf.presenter.algorithms.advanced.LeastMedianOfSquaresEstimator +de.wwwu.awolf.presenter.algorithms.advanced.RepeatedMedianEstimator +de.wwwu.awolf.presenter.algorithms.advanced.TheilSenEstimator +de.wwwu.awolf.presenter.algorithms.naiv.NaivLeastMedianOfSquaresEstimator +de.wwwu.awolf.presenter.algorithms.naiv.NaivRepeatedMedianEstimator +de.wwwu.awolf.presenter.algorithms.naiv.NaivTheilSenEstimator \ No newline at end of file diff --git a/src/main/resources/Thumbs.db b/src/main/resources/Thumbs.db new file mode 100644 index 0000000000000000000000000000000000000000..d34564893f5c20f3bc61ac680dda206783908fe6 GIT binary patch literal 34304 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 TmbLV(Rak!T|7lMCuU6!L*=mqS literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/export.png b/src/main/resources/icons/export.png new file mode 100644 index 0000000000000000000000000000000000000000..7c66e242840bbe3071944792926d67fc5afc7207 GIT binary patch literal 1328 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ za0`PlBg3pY5)UZBMdtX&_c`0Sw1aLh>S&L8we)zGQ>=|fM(yF< z3l@YqF7$G|(0nyaa8E}?*3oW-rWK(guP>SlY;l_E>hF~6|8?&g?TqWcE`9uP#>n63 z=1iY|=bxRN{Krn<#H5qP|4)9NeP*+#8nY5E(0P-g#%P9_+d9`QGxi$xFYK2KA2h8H z^~`yu@?sbq6g`4&o@+k| zRH~rqI=4RI%;VE6K-DTfw|@rM0hO9WO+DBCl8J%KQCYO~U$E-+xn>~IuBD$BMgeta zuw*}X{>sqcBoG?-+&*@ptT#}wLn-8Wvn^2Bm3OkIWS?j%yD%h}u)LSNHHlM9({5w1 z>k0-3GZyb-+fsU}8u74FXd6_1ky_k z9T?L0{(PDd8uIx2ZA)+GYsHM3)+{lsf7tej?wpqSvzpmjYfnei~lWOent6b!-RkkN%6cjdpy?w#!xYz1bGSwyrNS zL~{2@vAf6rPC3;#!&Nm+{e$Su+=^)}=eFPB{NwwLbq@2r19z|a$a61FD`eiS+_SH7 zT7&fUN1oR=DL*Te-M2L1=Jx~cJEqKMsAD|$z|L+~O6MES^`g@sSiNa)N}bJi`uNO) zZy)KiZhL(1qxqUm_sn;!zdN(<=kZO9g}Yw~oc}HU zfk)T!;5ONc)&l9}=U%?Pll}8bbeYojHw6vPt(&DS^5^ad72YCP?mcsRqkQN6ovF*7 z7q5TVu>H-_JDb?G?2apa&?}J7TOT=lM)O9wcmHqZ9yq$g`IPVO`yVpzyl4D<{qEO> z*9E(u@k`2XKQno6!eS5ka?R^X1v2Jpuh>_FyI=*op4J=j`Ke^{v+MyDc?_PeelF{r G5}E*&093UA literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/frame-2.png b/src/main/resources/icons/frame-2.png new file mode 100644 index 0000000000000000000000000000000000000000..2b6f862a491499cb9b7858bb7ca698fcdd208e1e GIT binary patch literal 764 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 r4d%&4#oDKcv3N>c<25(+pudvX`ORH>qMH!4nJ za0`PlBg3pY5)2H?TRdGHLn`LHy|Xt+`gTuUAmJUO#8&OxcXap}fd{f1taY|kvauS`ObOnH58f6q=*T=~{}FN61K#*Si! zM=}i->>k6~Pe4oZWfdu=rZz!Gdb{KOAzs zGWF@z521IwGfR{b#DD(ZNK!RF7jt%8ng zfV5_(tYZ&|-tAw=(ylGDI+LNh;)myvt_7Y9a&<|DN-Lxr+V2bcDz6X*(uTfDE2MyQ zV5#y7As}5?s-z+9@V?!C{^tKm1t~fA`5%PtKD{z^W{$+^#^2`{&o`VtaK7t!EXzFR z<;CyrGrSA2dA7x5-hGpQ2};|1KgF+z5H4F)Bn(_D8=0} zEpt?@g{S=NusFT`M`3?}^!SwANHmrIzomM7o&J!hHH578a|c|R_kXGkbwQJl`z zcm4E3=l^<)i@WsLL%)>$xbU7qp*vyo!Smm%eq8v^z!0`l-0^)ozZ9dxR}Pgnek(?Y zpByP|{85Yto^UK_(E`F+XSiz1@j_-f7DdlAG4s@mD&3D^W^2Af*|vL1)d$0>4yUI9fzv<6nkn`6X}c?mfSH=X M)78&qol`;+098V>-T(jq literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/plot.png b/src/main/resources/icons/plot.png new file mode 100644 index 0000000000000000000000000000000000000000..65e75509eb32e1cf67cf20871abd4ef399790572 GIT binary patch literal 1735 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 dB_$X5z9 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..0df0561 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,14 @@ +# Root logger option +log4j.rootLogger=DEBUG, file, stdout +# Direct log messages to a log file +log4j.appender.file=org.apache.log4j.RollingFileAppender +log4j.appender.file.File=D:\\Git\\master-implementierung\\LinearRegressionTool\\log4j\\app.log +log4j.appender.file.MaxFileSize=10MB +log4j.appender.file.MaxBackupIndex=10 +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} [%t] - %m%n +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} [%t] - %m%n diff --git a/src/main/resources/style/style.css b/src/main/resources/style/style.css new file mode 100644 index 0000000..06c19e9 --- /dev/null +++ b/src/main/resources/style/style.css @@ -0,0 +1,340 @@ + +/** + * JavaFX CSS Brume, light grey/white theme. + */ +.root { + -fx-background-color: derive(#1d1d1d,20%); +} + + +.titled-pane { + -fx-effect: dropshadow(three-pass-box, #9F9F9F, 15, 0, 0, 0); + -fx-animated: true; + -fx-text-fill: #505050; +} + +.titled-pane .title { + -fx-background-radius: 0, 0, 0; + -fx-font: bold 14px System; + -fx-padding: 0.3em 0.833333em 0.35em 0.833333em; + + /* stop the background from being blue on focus */ + -fx-background-color: ladder( + red, + black 20%, + derive(#d0d0d0,-30%) 30% + ), + linear-gradient( + to bottom, + ladder(#d0d0d0, + derive(#d0d0d0,80%) 60%, + white 82%) 0%, + ladder(#d0d0d0, + derive(#d0d0d0,20%) 10%, + derive(#d0d0d0,-10%) 80%) 100% + ), linear-gradient( + to bottom, + derive(#d0d0d0,34%) 0%, + derive(#d0d0d0,-18%) 100% + ); +} + +.titled-pane:focused > .title > .arrow-button .arrow { + /* do not highlight the title pane arrow on focus */ + -fx-background-color: #606060; +} + +.tree-cell:odd { + /* shade every other line in a tree view */ + -fx-background-color: derive(-fx-control-inner-background,-5%); +} + +/* the previous css to shade every other cell overrides the selection + colour, so put it back for odd cells */ +.tree-view:focused .tree-cell:filled:focused:selected:odd{ + -fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-selection-bar; +} + +/* if you have labels within your tree nodes, then make the text light so + that it stands out from the selection colour. */ +.tree-view:focused .tree-cell:filled:focused:selected:odd .label { + -fx-text-fill: -fx-light-text-color; +} +.tree-view:focused .tree-cell:filled:focused:selected:even .label { + -fx-text-fill: -fx-light-text-color; +} + +.button { + /* buttons do not have rounded corners */ + -fx-background-radius: 0; + -fx-label-padding: 0.3em 0.833333em 0.35em 0.833333em; +} + +.menu-button { + /* the left side of the menu button is not rounded */ +} + +.menu-button .label { + -fx-label-padding: 0.23em 0.2em 0.24em 0.2em; +} + +.combo-box-base { + /* the left side of the combo box is not rounded */ + -fx-background-radius: 0 5 5 0, 0 5 5 0, 0 4 4 0, 0 3 3 0; +} + +.combo-box .list-cell { + /* -fx-padding: 6 6 6 6; */ +} + +/* make the tooltip a slightly transparent black rectange with white text */ +.tooltip { + -fx-background-color: black; + -fx-opacity: 0.6; + -fx-background-insets: 0,1,2; + -fx-background-radius: 0; + -fx-padding: 0.333333em 0.666667em 0.333333em 0.666667em; /* 4 8 4 8 */ + -fx-wrap-text:false; +} +.tooltip .label { + -fx-text-fill: white; +} +/* remove the turned page effect */ +.page-corner { + -fx-padding: 0; + -fx-background-color: null; + -fx-shape: null; + -fx-effect: null; +} + +.toggle-button { + -fx-background-radius: 0; + -fx-label-padding: 0.3em 0.833333em 0.35em 0.833333em; +} +.check-box { + -fx-background-radius: 0; +} + +.choice-box { + /* TODO add padding */ + -fx-background-radius: 0; +} +/* Dark */ +.background { + -fx-background-color: derive(#1d1d1d,20%); +} + +.label { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-opacity: 0.6; +} + +.label-bright { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-opacity: 1; +} + +.label-header { + -fx-font-size: 32pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 1; +} + +.table-view { + -fx-base: #1d1d1d; + -fx-control-inner-background: #1d1d1d; + -fx-background-color: #1d1d1d; + -fx-table-cell-border-color: transparent; + -fx-table-header-border-color: transparent; + -fx-padding: 5; +} + +.table-view .column-header-background { + -fx-background-color: transparent; +} + +.table-view .column-header, .table-view .filler { + -fx-size: 35; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-border-color: + transparent + transparent + derive(-fx-base, 80%) + transparent; + -fx-border-insets: 0 10 1 0; +} + +.table-view .column-header .label { + -fx-font-size: 20pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: -fx-focus-color; +} + +.split-pane:horizontal > .split-pane-divider { + -fx-border-color: transparent #1d1d1d transparent #1d1d1d; + -fx-background-color: transparent, derive(#1d1d1d,20%); +} + +.split-pane { + -fx-padding: 1 0 0 0; +} + +.menu-bar { + -fx-background-color: derive(#1d1d1d,20%); +} + +.context-menu { + -fx-background-color: derive(#1d1d1d,50%); +} + +.menu-bar .label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 0.9; +} + +.menu .left-container { + -fx-background-color: black; +} + +.text-field { + -fx-font-size: 12pt; + -fx-font-family: "Segoe UI Semibold"; +} + + +/* + * Metro style Push Button + * Author: Pedro Duque Vieira + * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ + */ +.button { + -fx-padding: 5 22 5 22; + -fx-border-color: #e2e2e2; + -fx-border-width: 2; + -fx-background-radius: 0; + -fx-background-color: #1d1d1d; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.button:hover { + -fx-background-color: #3a3a3a; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: white; + -fx-text-fill: #1d1d1d; +} + +.button:focused { + -fx-border-color: white, white; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 0, 0; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #1d1d1d; + -fx-text-fill: white; +} + +.button:default { + -fx-background-color: -fx-focus-color; + -fx-text-fill: #ffffff; +} + +.button:default:hover { + -fx-background-color: derive(-fx-focus-color,30%); +} +/* Console */ +.text-area { + -fx-font-family: Consolas; + -fx-font-size: 15; + -fx-text-fill: #63f542; + -fx-display-caret:true; +} + +.text-area .content { + -fx-background-color: #000000; +} + +/* tabs */ +.tab-pane:top *.tab-header-area { + -fx-background-insets: 0, 0 0 1 0; + /* -fx-padding: 0.416667em 0.166667em 0.0em 0.833em; /* 5 2 0 10 */ + -fx-padding: 0.416667em 0.166667em 0.0em 0.0em; /* overridden as 5 2 0 0 */ +} + +.tab-pane .tab-header-area .tab-header-background { + -fx-opacity: 0; +} + +.tab-pane { + -fx-tab-min-width:120px; + -fx-tab-min-height:50px; + -fx-tab-max-height:50px; +} + +.tab-pane .tab { + -fx-background-color: derive(#1d1d1d,20%); + -fx-background-insets: 0 1 0 1,0,0; + -fx-label-padding: 0.25em 0.25em 0.25em 0.25em; +} + +.tab .tab-label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 0.9; + -fx-display-caret:true; +} + +.tab-pane .tab:selected { + -fx-background-color: -fx-focus-color; +} + +.algoPane { + -fx-background-color: white; +} + +.algoPane .label { + -fx-text-fill: black; + -fx-font-size: 16pt; + -fx-font-family: "Segoe UI Light"; + -fx-opacity: 0.9; +} + +.default-color0.chart-series-line { -fx-stroke: transparent; } +.default-color1.chart-series-line { -fx-stroke: red; } + +.default-color0.chart-line-symbol { + -fx-background-color: white, green; +} +.default-color1.chart-line-symbol { + -fx-background-color: transparent, transparent; +} + +.default-color0.chart-legend-item-symbol{ + -fx-background-color: green; + } +.default-color1.chart-legend-item-symbol{ + -fx-background-color: red; + } \ No newline at end of file diff --git a/src/main/resources/views/AlgorithmTab.fxml b/src/main/resources/views/AlgorithmTab.fxml new file mode 100644 index 0000000..d179ffb --- /dev/null +++ b/src/main/resources/views/AlgorithmTab.fxml @@ -0,0 +1,64 @@ + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/views/MainFrame.fxml b/src/main/resources/views/MainFrame.fxml new file mode 100644 index 0000000..2e9c7c3 --- /dev/null +++ b/src/main/resources/views/MainFrame.fxml @@ -0,0 +1,22 @@ + + + + + + +
+ + + + +
+ + + + + +
diff --git a/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimatorTest.java b/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimatorTest.java new file mode 100644 index 0000000..44468aa --- /dev/null +++ b/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/LeastMedianOfSquaresEstimatorTest.java @@ -0,0 +1,37 @@ +package de.wwwu.awolf.presenter.algorithms.advanced; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.presenter.algorithms.Algorithm; +import de.wwwu.awolf.presenter.algorithms.AlgorithmHandler; +import java.util.HashSet; +import java.util.Set; +import org.junit.Before; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 23.10.2017. + */ +public class LeastMedianOfSquaresEstimatorTest { + + private LeastMedianOfSquaresEstimator lms; + private Set lines; + private Line line; + + @Before + public void setUp() { + Double[] x = {18d, 24d, 30d, 34d, 38d}; + Double[] y = {18d, 26d, 30d, 40d, 70d}; + + lines = new HashSet<>(); + + for (int i = 0; i < 5; i++) { + lines.add(new Line(x[i], y[i])); + } + line = AlgorithmHandler.getInstance().runAlgorithmByType(Algorithm.Type.LMS, lines); + } + + +} \ No newline at end of file diff --git a/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimatorTest.java b/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimatorTest.java new file mode 100644 index 0000000..b68b8aa --- /dev/null +++ b/src/test/java/de/wwwu/awolf/presenter/algorithms/advanced/TheilSenEstimatorTest.java @@ -0,0 +1,31 @@ +package de.wwwu.awolf.presenter.algorithms.advanced; + +import de.wwwu.awolf.model.Line; +import java.util.LinkedList; +import java.util.List; +import org.junit.Before; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 23.10.2017. + */ +public class TheilSenEstimatorTest { + + + @Before + public void setUp() { + Double[] x = {18d, 24d, 30d, 34d, 38d}; + Double[] y = {18d, 26d, 30d, 40d, 70d}; + + List lines = new LinkedList<>(); + + for (int i = 0; i < 5; i++) { + lines.add(new Line(x[i], y[i])); + } + } + + +} \ No newline at end of file diff --git a/src/test/java/de/wwwu/awolf/presenter/util/FastElementSelectorTest.java b/src/test/java/de/wwwu/awolf/presenter/util/FastElementSelectorTest.java new file mode 100644 index 0000000..7fe85a9 --- /dev/null +++ b/src/test/java/de/wwwu/awolf/presenter/util/FastElementSelectorTest.java @@ -0,0 +1,33 @@ +package de.wwwu.awolf.presenter.util; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 23.10.2017. + */ +public class FastElementSelectorTest { + + + @Test + public void randomizedSelect() throws Exception { + Double[] array = {1d, 2d, 3d, 4d}; + ArrayList list = new ArrayList<>(Arrays.asList(array)); + assertEquals(FastElementSelector.randomizedSelect(list, 0), 1d, 0.00001); + assertEquals(FastElementSelector.randomizedSelect(list, 2), 2d, 0.00001); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testIndexOutOfBoundsException() { + FastElementSelector.randomizedSelect(Collections.emptyList(), 42); + } + +} \ No newline at end of file diff --git a/src/test/java/de/wwwu/awolf/presenter/util/IntersectionCounterTest.java b/src/test/java/de/wwwu/awolf/presenter/util/IntersectionCounterTest.java new file mode 100644 index 0000000..ebbea50 --- /dev/null +++ b/src/test/java/de/wwwu/awolf/presenter/util/IntersectionCounterTest.java @@ -0,0 +1,40 @@ +package de.wwwu.awolf.presenter.util; + +import static org.junit.Assert.assertEquals; + +import de.wwwu.awolf.model.Line; +import de.wwwu.awolf.model.LineModel; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; + +/** + * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. + * + * @Author: Armin Wolf + * @Email: a_wolf28@uni-muenster.de + * @Date: 23.10.2017. + */ +public class IntersectionCounterTest { + + private LineModel lineModel; + + @Before + public void setUp() throws Exception { + lineModel = new LineModel(); + lineModel.addLine(new Line(3, 13, 10, 3)); + lineModel.addLine(new Line(1, 9, 1, 9)); + lineModel.addLine(new Line(1, 12, 4, 6)); + for (Line l : lineModel.getLines()) { + System.out.println("Steigung: " + l.getM() + "\t y-Achsenabschnitt: " + l.getB()); + } + } + + @Test + public void run() throws Exception { + IntersectionComputer instance = IntersectionComputer.getInstance(); + assertEquals(3, IntersectionComputer.getInstance() + .compute(new ArrayList<>(lineModel.getLines()), -9999, 9999).size()); + } + +} \ No newline at end of file