From b2eafebd5609f4e0da9fb60fcaad12995ee4f412 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 16:41:52 +0100 Subject: [PATCH 001/214] Adjusted german README_de.txt --- configuration/aws-automation/README_de.txt | 174 +++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 configuration/aws-automation/README_de.txt diff --git a/configuration/aws-automation/README_de.txt b/configuration/aws-automation/README_de.txt new file mode 100644 index 00000000000..f9834f797b6 --- /dev/null +++ b/configuration/aws-automation/README_de.txt @@ -0,0 +1,174 @@ +Beschreibung des Skripts: + +0. Voraussetzungen für Verwendung + 0.1 AWS CLI konfigurieren + 0.2 Standardwerte anpassen + 0.3 Bedienung anzeigen +1. Aufbau + 1.1 Dateien und ihre Aufgaben + 1.2 Szenarios +2. Funktionalität + 2.1 Initialisierung von benötigten Variablen + 2.2 Fehlerbehandlung + 2.3 Meldungsausgabe +3. TODO + 3.1 Fehlerbehebungen + 3.2 Verbesserungen + 3.3 Erweiterungen + +0. Voraussetzungen für Verwendung + +- AWS CLI (https://aws.amazon.com/de/cli) +- Cygwin Packages: +-- tmux +-- jq +-- openssh +-- wget +-- curl + +0.1 AWS CLI konfigurieren + +In die Konsole muss "aws configure" eingegeben werden. Anschließend müssen Access Key und Secret Access Key +deines AWS Benutzers eingegeben werden. + +Falls noch nicht vorhanden, kann über die AWS IAM Konsole ein entsprechender Schlüssel generiert werden. +Dazu navigiert ein priviligierter Benutzer in der AWS Webkonsole zu folgendem Eintrag: +IAM-Konsole >> Users >> dein Benutzername >> Security Credentials >> Create access key + +0.2 Standardwerte anpassen + +Zur Vermeidung der wiederholten Eingabe von nutzerspezifischen Informationen, können in der Datei +aws_variables.sh bestimmte Standardwerte für Variablen festgelegt werden. Es ist zum Beispiel +möglich die standardmäßige Region festzulegen, den standardmäßigen Schlüsselname zur Verbindung zu einer +Instanz und weiteres. + +0.3 Bedienung anzeigen + +Das Skript zeigt über den Aufruf ./aws-setup.sh oder ./aws-setup.sh --help Hinweise zur Bedienung an. + +1. Aufbau + +1.1 Dateien und ihre Aufgaben + +aws-setup.sh: +Funktionen für Parameteraufnahme, Sourcing und Dokumentation. + +lib/functions_app.sh: +Funktionen für die Modifikation des Zustands der SAP Sailing Analytics Instanz (Eventerstellung, Passwortänderung, Apache-Konfiguration) + +lib/functions_ec2.sh: +Funktionen für die Erstellung von EC2-Instanzen und Abfrage von deren Attributen + +lib/functions_elb.sh: +Funktionen für die Erstellung Load Balancern, Listener, Rules und Target Groups + +lib/functions_io.sh: +Funktionen für die Verarbeitung von Nutzereingaben und Initialisieren von Variablen + +lib/functions_route53.sh: +Funktionen für die Erstellung von Route53 Einträgen + +lib/functions_tmux.sh: +Funktionen für die Ausführung von Befehlen über tmux und zum Aufbau der Benutzeroberfläche + +lib/functions_wrapper.sh: +Funktionen für die Fehlerverarbeitung von Funktionen und Vereinfachung von wiederholten Aufrufen + +lib/scenario_associate_alb.sh, +lib/scenario_associate_clb.sh, +lib/scenario_associate_elastic_ip.sh, +lib/scenario_instance.sh, +lib/scenarion_tail.sh: +Siehe skriptinterne Dokumentation + +lib/util_functions.sh: +Funktionen für die Erleichterung der Benutzereingabe, Validierungsfunktionen und sonstige potenziell relevanten Hilfsfunktionen + +lib/util_variables.sh: +Sonstiges Hilfsvariablen (Zeitstempel, Skriptname, etc.) + +lib/utils.sh: +Funktionen für die Ausgabe von farbigen Meldungen, Logging und Sourcing der restlichen Bash-Dateien + +1.2 Szenarios + +Szenarios sind gekapselte Ausführungseinheiten, die durch Orchestrierung von AWS- und instanzspezifischen +Funktionen einen bestimmten Use-Case automatisieren. + +Szenarios beinhalten folgende Funktionen: + +- Funktion zum Start des Ausführung des Szenarios +- Funktion zur Überprüfung von Vorbedingungen (Abhängigkeiten von Paketen oder Umgebungsvariablen) [optional] +- Funktion zur Sicherstellung der Initialisierung von benötigen Variablen +- Funktion zur Ausführung der Programmlogik + +2. Funktionalität + +2.1 Initialisierung von benötigten Variablen + +Jedes Szenario benötigt für seine Ausführung bestimmte inititalisierte Variablen. Die Zuweisung eines Werts zu einer Variable erfolgt +entweder beim Start des Skripts über die Mitgabe eines Parameters oder falls kein Parameter übergeben worden ist, über die Eingabe des +Benutzers nach entsprechender Aufforderung. Wenn der Benutzer zur Eingabe eines Werts für eine Variable aufgefordert wird, wird automatisch +ein Standardwert aus der Datei aws_variables.sh vorgeschlagen. Dies erleichtert die Benutzereingabe, da sich bestimmte Werte, wie zum Beispiel der Name +des Keypairs zur Verbindung einer Instanz selten ändern. In der Datei werden außerdem regionspezifische Variablenwerte, wie zum Beispiel +die IDs der Sicherheitsgruppen oder Images festgelegt. + +2.2 Fehlerbehandlung + +Um die erfolgreiche Ausführung einer Funktion zu überprüfen werden verschiedene Strategien verfolgt: + +1. Überprüfung des Rückgabewerts einer Funktion + +Diese Strategie wird bei AWS-spezifischen Funktionen angewandt. Bei Aufruf eines AWS-Kommandos (z.B. aws elb create-load-balancer) wird +bei Erfolg der Rückgabewert 0 zurückgegeben und bei Auftritt eines Fehlers der Rückgabewert 1. + +2. Überpüfen des HTTP-Codes + +Bei Aufruf eines curl-Kommandos wird die Option -w "\n%{http_code}" mitübergeben. Dabei werden Response- und HTTP-Code auf zwei Zeilen +aufgeteilt und damit ermöglicht, dass nach Aufruf der Funktion die Zuordnung zu zwei verschiedenen Variablen erfolgt. Es wird dann verglichen, ob der +HTTP-Statuscode mit dem erwarteten Ergebnis übereinstimmt. + +2.3 Meldungsausgabe + +Innerhalb von Methoden wird der Nutzer über Erfolg oder Misserfolg benachrichtigt. Die Ausgabe erfolgt über den +Standardfehlerkanal "stderr". + +3. TODO + +3.1 Fehlerbehebungen + +- Nutzer erstellen scheitert bisher mit "Precondition failed" (Nutzername erfüllt Richtlinie nicht) + +3.2 Verbesserungen + +- run-instance Funktion nach "aws_functions.sh" faktorisieren +- Anpassung der Dateierzeugung an Linux ("crlf" ersetzen) + +3.3 Erweiterungen + +- list-Funktion mit Anzeige aller Instanznamen und deren DNS, Instanz-ID, etc. damit --tail nicht die Web-GUI erfordert +- Detailiertere Ausgabe von Zwischenergebnisse des Skripts über die Konsole +- weitere Szenarios hinzufügen (Multi-Instanz-Setup, Shutdown, Replica Mgmt.) +- regionspezifisches Sourcing von Dateien mit Variablen (Auskommentieren nicht mehr notwendig) + + + +Getestet unter Microsoft Windows [Version 10.0.10586]: +- Cygwin 2.8.2(0.313/5/3) +- aws-cli/1.11.129 +- tmux 2.8.2(0.313/5/3) +- openssh 7.5p1-1 +- wget 1.19.1 +- curl 7.53.0 + +=> Funktioniert (Route53-Eintrag Erstellung konnte bisher nicht getestet werden) + +Getestet unter Microsoft Windows [Version 6.3.96001]: +- Cygwin 2.9.0(0.318/5/3) +- aws-cli/1.11.166 +- tmux 2.4 +- openssh 7.4p1 +- wget 1.19.1 +- curl 7.55.1-1 + +=> Funktioniert (Route53-Eintrag Erstellung konnte bisher nicht getestet werden) From 9b694513009a541437a18849f39c1a277d924d72 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 17:23:41 +0100 Subject: [PATCH 002/214] Translated README_de.txt to README_en.txt --- configuration/aws-automation/README_en.txt | 164 +++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 configuration/aws-automation/README_en.txt diff --git a/configuration/aws-automation/README_en.txt b/configuration/aws-automation/README_en.txt new file mode 100644 index 00000000000..8954b372f8e --- /dev/null +++ b/configuration/aws-automation/README_en.txt @@ -0,0 +1,164 @@ +Description: + +0. Requirements for use + 0.1 Configure AWS CLI + 0.2 Adjust default values + 0.3 Show manual + +1. Structure + 1.1 Files and their tasks + 1.2 Scenarios + +2. Functionality + 2.1 Initialization of required variables + 2.2 Error handling + +3. TODO + 3.1 Bug fixes + 3.2 Improvements + 3.3 Extensions + +0. Requirements for use + +- AWS CLI (https://aws.amazon.com/de/cli) +- Cygwin Packages: +-- tmux +-- jq +-- openssh +-- wget +-- curl + +0.1 Configure AWS CLI + +Enter "aws configure" into the console. Enter your Access Key and Secret Access Key of your AWS user. +If not already available, a corresponding key can be generated via the AWS Web console. + +To do this, a privileged user navigates to the following entry within the AWS Web console: +IAM Console >> Users >> Your Username >> Security Credentials >> Create access key + +0.2 Adjust default values + +To avoid repeated input of user-specific information, the file aws_variables.sh contains default values ​​ +for variables. It is possible to set the default region and default key name for instance. + +0.3 Show manual + +Call ./aws-setup.sh or ./aws-setup.sh --help to display instructions for usage. + +1. Structure + +1.1 Files and their tasks + +aws-setup.sh: +Functions for parameter recording, sourcing and documentation. Entry point of program. + +lib/functions_app.sh: +Functions for modifying the state of the SAP Sailing Analytics instance (event creation, password change, apache configuration). + +lib/functions_ec2.sh: +Functions for creating EC2 instances and querying their attributes. + +lib/functions_elb.sh: +Functions for creating load balancers, listeners, rules and target groups. + +lib/functions_io.sh: +Functions for processing user input and initializing variables. + +lib/functions_route53.sh: +Functions for creating Route53 entries. + +lib/functions_tmux.sh: +Functions for executing commands via tmux and for constructing the user interface. + +lib/functions_wrapper.sh: +Functions for error handling of functions and simplification of repeated calls. + +lib/scenario_associate_alb.sh, +lib/scenario_associate_clb.sh, +lib/scenario_associate_elastic_ip.sh, +lib/scenario_instance.sh, +lib/scenarion_tail.sh: +See script-internal documentation + +lib/util_functions.sh: +Features to facilitate user input, validation features and other potentially relevant helper functions. + +lib/util_variables.sh: +Miscellaneous auxiliary variables (time stamp, script name, etc.) + +lib/utils.sh: +Functions for the output of colored messages, logging and sourcing of remaining bash files + +1.2 Szenarios + +Scenarios are encapsulated execution units that are orchestrated by AWS and instance-specific functions +to automate a specific use case. + +Scenarios include the following functions: + +- Function to start the execution of the scenario +- Function to check preconditions (dependencies of packages or environment variables) [optional] +- Function to ensure the initialization of variables needed +- Function to execute the program logic + +2. Functionality + +2.1 Initialization of required variables + +Each scenario requires certain initialized variables for its execution. The assignment of a value to a variable takes place +either at the start of the script via the input of a parameter or if no parameter has been transferred, by input of the user after being prompted. +When the user is prompted to enter a value for a variable, he is being offered a default value out of the aws_variables.sh file. +This facilitates user input because certain values, such as the standard key name to connect to an instance don't change very often. + +2.2 Error handling + +To check the successful execution of a function, various strategies are used: + +1. Checking the return value of a function + +This strategy is applied to AWS-specific functions. When calling an AWS command (e.g. aws elb create-load-balancer) +the return value is either 0 (error occured) or 1 (no error occured). + +2. Checking the HTTP status code + +When a curl command is called, the option -w "\ n% {http_code}" is also passed as a parameter. In doing so, response and HTTP code can be assigned +to two different variables, allowing separate processing. The HTTP status code can then be compared to the expected result, without loosing information +about the actual response. + +3. TODO + +3.1 Bug fixes + +- Creating user fails with message "Precondition failed (username does not meet policy)" + +3.2 Improvements + +- Factorize run-instance() function + +3.3 Extensions + +- list() function with output of all instance names and their DNS name, instance id, etc. so that --tail does not require the web GUI anymore +- More detailed output of intermediate results of the script via the console +- Add more scenarios (Multi-Instance-Setup, Shutdown, Replica Mgmt.) +- region-specific sourcing of files with variables, so that commenting out is no longer necessary + + +Tested under Microsoft Windows [Version 10.0.10586]: +- Cygwin 2.8.2(0.313/5/3) +- aws-cli/1.11.129 +- tmux 2.8.2(0.313/5/3) +- openssh 7.5p1-1 +- wget 1.19.1 +- curl 7.53.0 + +=> Works (Route53 entry creation could not be tested yet) + +Tested under Microsoft Windows [Version 6.3.96001]: +- Cygwin 2.9.0(0.318/5/3) +- aws-cli/1.11.166 +- tmux 2.4 +- openssh 7.4p1 +- wget 1.19.1 +- curl 7.55.1-1 + +=> Works (Route53 entry creation could not be tested yet) From 1c0167a6240c7de224dc58d06f686e6a34678e8f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 17:24:32 +0100 Subject: [PATCH 003/214] Delete README_de.txt --- configuration/aws-automation/README_de.txt | 174 --------------------- 1 file changed, 174 deletions(-) delete mode 100644 configuration/aws-automation/README_de.txt diff --git a/configuration/aws-automation/README_de.txt b/configuration/aws-automation/README_de.txt deleted file mode 100644 index f9834f797b6..00000000000 --- a/configuration/aws-automation/README_de.txt +++ /dev/null @@ -1,174 +0,0 @@ -Beschreibung des Skripts: - -0. Voraussetzungen für Verwendung - 0.1 AWS CLI konfigurieren - 0.2 Standardwerte anpassen - 0.3 Bedienung anzeigen -1. Aufbau - 1.1 Dateien und ihre Aufgaben - 1.2 Szenarios -2. Funktionalität - 2.1 Initialisierung von benötigten Variablen - 2.2 Fehlerbehandlung - 2.3 Meldungsausgabe -3. TODO - 3.1 Fehlerbehebungen - 3.2 Verbesserungen - 3.3 Erweiterungen - -0. Voraussetzungen für Verwendung - -- AWS CLI (https://aws.amazon.com/de/cli) -- Cygwin Packages: --- tmux --- jq --- openssh --- wget --- curl - -0.1 AWS CLI konfigurieren - -In die Konsole muss "aws configure" eingegeben werden. Anschließend müssen Access Key und Secret Access Key -deines AWS Benutzers eingegeben werden. - -Falls noch nicht vorhanden, kann über die AWS IAM Konsole ein entsprechender Schlüssel generiert werden. -Dazu navigiert ein priviligierter Benutzer in der AWS Webkonsole zu folgendem Eintrag: -IAM-Konsole >> Users >> dein Benutzername >> Security Credentials >> Create access key - -0.2 Standardwerte anpassen - -Zur Vermeidung der wiederholten Eingabe von nutzerspezifischen Informationen, können in der Datei -aws_variables.sh bestimmte Standardwerte für Variablen festgelegt werden. Es ist zum Beispiel -möglich die standardmäßige Region festzulegen, den standardmäßigen Schlüsselname zur Verbindung zu einer -Instanz und weiteres. - -0.3 Bedienung anzeigen - -Das Skript zeigt über den Aufruf ./aws-setup.sh oder ./aws-setup.sh --help Hinweise zur Bedienung an. - -1. Aufbau - -1.1 Dateien und ihre Aufgaben - -aws-setup.sh: -Funktionen für Parameteraufnahme, Sourcing und Dokumentation. - -lib/functions_app.sh: -Funktionen für die Modifikation des Zustands der SAP Sailing Analytics Instanz (Eventerstellung, Passwortänderung, Apache-Konfiguration) - -lib/functions_ec2.sh: -Funktionen für die Erstellung von EC2-Instanzen und Abfrage von deren Attributen - -lib/functions_elb.sh: -Funktionen für die Erstellung Load Balancern, Listener, Rules und Target Groups - -lib/functions_io.sh: -Funktionen für die Verarbeitung von Nutzereingaben und Initialisieren von Variablen - -lib/functions_route53.sh: -Funktionen für die Erstellung von Route53 Einträgen - -lib/functions_tmux.sh: -Funktionen für die Ausführung von Befehlen über tmux und zum Aufbau der Benutzeroberfläche - -lib/functions_wrapper.sh: -Funktionen für die Fehlerverarbeitung von Funktionen und Vereinfachung von wiederholten Aufrufen - -lib/scenario_associate_alb.sh, -lib/scenario_associate_clb.sh, -lib/scenario_associate_elastic_ip.sh, -lib/scenario_instance.sh, -lib/scenarion_tail.sh: -Siehe skriptinterne Dokumentation - -lib/util_functions.sh: -Funktionen für die Erleichterung der Benutzereingabe, Validierungsfunktionen und sonstige potenziell relevanten Hilfsfunktionen - -lib/util_variables.sh: -Sonstiges Hilfsvariablen (Zeitstempel, Skriptname, etc.) - -lib/utils.sh: -Funktionen für die Ausgabe von farbigen Meldungen, Logging und Sourcing der restlichen Bash-Dateien - -1.2 Szenarios - -Szenarios sind gekapselte Ausführungseinheiten, die durch Orchestrierung von AWS- und instanzspezifischen -Funktionen einen bestimmten Use-Case automatisieren. - -Szenarios beinhalten folgende Funktionen: - -- Funktion zum Start des Ausführung des Szenarios -- Funktion zur Überprüfung von Vorbedingungen (Abhängigkeiten von Paketen oder Umgebungsvariablen) [optional] -- Funktion zur Sicherstellung der Initialisierung von benötigen Variablen -- Funktion zur Ausführung der Programmlogik - -2. Funktionalität - -2.1 Initialisierung von benötigten Variablen - -Jedes Szenario benötigt für seine Ausführung bestimmte inititalisierte Variablen. Die Zuweisung eines Werts zu einer Variable erfolgt -entweder beim Start des Skripts über die Mitgabe eines Parameters oder falls kein Parameter übergeben worden ist, über die Eingabe des -Benutzers nach entsprechender Aufforderung. Wenn der Benutzer zur Eingabe eines Werts für eine Variable aufgefordert wird, wird automatisch -ein Standardwert aus der Datei aws_variables.sh vorgeschlagen. Dies erleichtert die Benutzereingabe, da sich bestimmte Werte, wie zum Beispiel der Name -des Keypairs zur Verbindung einer Instanz selten ändern. In der Datei werden außerdem regionspezifische Variablenwerte, wie zum Beispiel -die IDs der Sicherheitsgruppen oder Images festgelegt. - -2.2 Fehlerbehandlung - -Um die erfolgreiche Ausführung einer Funktion zu überprüfen werden verschiedene Strategien verfolgt: - -1. Überprüfung des Rückgabewerts einer Funktion - -Diese Strategie wird bei AWS-spezifischen Funktionen angewandt. Bei Aufruf eines AWS-Kommandos (z.B. aws elb create-load-balancer) wird -bei Erfolg der Rückgabewert 0 zurückgegeben und bei Auftritt eines Fehlers der Rückgabewert 1. - -2. Überpüfen des HTTP-Codes - -Bei Aufruf eines curl-Kommandos wird die Option -w "\n%{http_code}" mitübergeben. Dabei werden Response- und HTTP-Code auf zwei Zeilen -aufgeteilt und damit ermöglicht, dass nach Aufruf der Funktion die Zuordnung zu zwei verschiedenen Variablen erfolgt. Es wird dann verglichen, ob der -HTTP-Statuscode mit dem erwarteten Ergebnis übereinstimmt. - -2.3 Meldungsausgabe - -Innerhalb von Methoden wird der Nutzer über Erfolg oder Misserfolg benachrichtigt. Die Ausgabe erfolgt über den -Standardfehlerkanal "stderr". - -3. TODO - -3.1 Fehlerbehebungen - -- Nutzer erstellen scheitert bisher mit "Precondition failed" (Nutzername erfüllt Richtlinie nicht) - -3.2 Verbesserungen - -- run-instance Funktion nach "aws_functions.sh" faktorisieren -- Anpassung der Dateierzeugung an Linux ("crlf" ersetzen) - -3.3 Erweiterungen - -- list-Funktion mit Anzeige aller Instanznamen und deren DNS, Instanz-ID, etc. damit --tail nicht die Web-GUI erfordert -- Detailiertere Ausgabe von Zwischenergebnisse des Skripts über die Konsole -- weitere Szenarios hinzufügen (Multi-Instanz-Setup, Shutdown, Replica Mgmt.) -- regionspezifisches Sourcing von Dateien mit Variablen (Auskommentieren nicht mehr notwendig) - - - -Getestet unter Microsoft Windows [Version 10.0.10586]: -- Cygwin 2.8.2(0.313/5/3) -- aws-cli/1.11.129 -- tmux 2.8.2(0.313/5/3) -- openssh 7.5p1-1 -- wget 1.19.1 -- curl 7.53.0 - -=> Funktioniert (Route53-Eintrag Erstellung konnte bisher nicht getestet werden) - -Getestet unter Microsoft Windows [Version 6.3.96001]: -- Cygwin 2.9.0(0.318/5/3) -- aws-cli/1.11.166 -- tmux 2.4 -- openssh 7.4p1 -- wget 1.19.1 -- curl 7.55.1-1 - -=> Funktioniert (Route53-Eintrag Erstellung konnte bisher nicht getestet werden) From 341bfd07952e46497a235549f9ca3e87a25090cd Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 17:24:47 +0100 Subject: [PATCH 004/214] Delete README.txt --- configuration/aws-automation/README.txt | 169 ------------------------ 1 file changed, 169 deletions(-) delete mode 100644 configuration/aws-automation/README.txt diff --git a/configuration/aws-automation/README.txt b/configuration/aws-automation/README.txt deleted file mode 100644 index 36cd7a8e0cf..00000000000 --- a/configuration/aws-automation/README.txt +++ /dev/null @@ -1,169 +0,0 @@ -Beschreibung des Skripts: - -0. Voraussetzungen f�r Nutzung des Skripts - -- AWS CLI (https://aws.amazon.com/de/cli) -- Cygwin Packages: --- tmux --- jq --- openssh --- wget --- curl - -0.1 AWS CLI konfigurieren - -"aws configure" durchf�hren. -Falls noch kein Keypair vorhanden: - -IAM-Konsole >> Users >> dein Benutzername >> Security Credentials >> Create access key - -0.2 Default-Werte anpassen - -Default-Werte in "aws_variables.sh" anpassen (key_name, key_file), damit diese nicht -immer neu eingegeben werden m�ssen. Testweise erst einmal in Region "eu-west-2" bleiben. - -key_name: Name des Keypairs zur Verbindung zur Instanz -key_file: Pfad zur Datei -... - -0.3 Skript ausprobieren - -./aws-setup.sh --help -./aws-setup instance-with-load-balancer - -Falls Fehler auftaucht: -Skript mit "-d"-Parameter starten und in den Logs der Instanz nachsehen -(./aws-setup.sh --tail --public-dns-name ec2-x...amazon.com) - -1. Aufbau: - -1.1 Dateien und ihre Hauptaufgaben: - -aws-setup-sh: -Parameteraufnahme, Main-Funktion, Dokumentation der Bedienung, Sourcing von lib/utils.sh - -lib/aws-functions.sh: -Funktionen f�r AWS (z.B. Load Balancer erstellen) und App-Instanz (z.B. Event erstellen) - -lib/aws-variables.sh: -Festlegung von regionspezifischen Konstanten (z.B Hosted Zone ID) und Defaultwerten (z.B Region) - -lib/scenario_instance.sh: -Erstellung einer Instanz und anschlie�endes Hinzuf�gen zu einem Load Balancer der �ber einen Route53 Eintrag -mit einer bestimmten Domain (z.B. wcs17.sapsailing.com) verkn�pft wird. - -lib/scenario_tail.sh: -Automatische Anzeige der Log-Dateien einer Instanz mithilfe von tmux - -lib/tmux_functions.sh: -Oberfl�chenkonstruktion, Vorbedingungstest, Befehlsausf�hrung - -lib/util_functions.sh: -Erleichterung der Benutzereingabe, Validierungsfunktionen, sonstige potenziell relevante Hilfsfunktionen - -lib/util_variables.sh: -Sonstiges Hilfsvariablen (Zeitstempel, Skriptname, etc.) - -lib/utils.sh: -Ausgabe von farbigen Meldungen, Logging (unbenutzt), Sourcing der restlichen Bash-Dateien im Verzeichnis - -1.2 Szenarios - -Szenarios sind gekapselte Ausf�hrungseinheiten, die durch Orchestrierung von AWS-Funktionen oder Bereitstellung -eigener Funktionalit�t einen Mehrwert f�r die Automatisierung darstellen. - -Szenarios beinhalten folgende Funktionen: - -- Funktion zum Start des Ausf�hrung des Szenarios -- Funktion zur �berpr�fung von Vorbedingungen (Abh�ngigkeiten von Paketen oder Umgebungsvariablen) [optional] -- Funktion zur Sicherstellung der Initialisierung von ben�tigen Variablen -- Funktion zur Ausf�hrung der Programmlogik - -2. Funktionalit�t - -2.1 Initialisierung von ben�tigten Variablen - -Jedes Szenario ben�tigt f�r seine Ausf�hrung bestimmte inititalisierte Variablen. Die Zuweisung eines Werts zu einer Variable erfolgt -entweder beim Start des Skripts �ber die Mitgabe eines Parameters oder falls kein Parameter �bergeben worden ist, �ber die Eingabe des -Benutzers nach entsprechender Aufforderung. -Wenn der Benutzer zur Eingabe eines Werts f�r eine Variable aufgefordert wird, wird automatisch ein Default-Wert aus der -Datei "aws_variables.sh" vorgeschlagen. Dies erleichtert die Benutzereingabe, da sich bestimmte Werte, wie z.B. der Name -des Keypairs zur Verbindung einer Instanz selten �ndern. In der Datei werden au�erdem regionspezifische Variablenwerte -(z.B. IDs der Sicherheitsgruppen oder Images) festgelegt. - -2.2 Fehlerbehandlung - -Um die erfolgreiche Ausf�hrung einer Funktion zu �berpr�fen werden verschiedene Strategien verfolgt: - -1. �berpr�fung des R�ckgabewerts einer Funktion - -Diese Strategie wird bei AWS-spezifischen Funktionen angewandt. Bei Aufruf eines AWS-Kommandos (z.B. aws elb create-load-balancer) wird -bei Erfolg der R�ckgabewert 0 zur�ckgegeben und bei Vorkommen eines Fehlers der R�ckgabewert 1. - -2. �berpr�fen des HTTP-Codes - -Bei Aufruf eines curl-Kommandos wird die Option -w "\n%{http_code}" mit�bergeben. Dabei werden Response- und HTTP-Code auf zwei Zeilen -aufgeteilt und damit erm�glicht, dass nach Aufruf der Funktion die Zuordnung zu zwei verschiedenen Variablen erfolgt. - -2.3 Meldungsausgabe - -Innerhalb von Methoden wird der Nutzer �ber Erfolg oder Misserfolg benachrichtigt. Die Ausgabe erfolgt �ber den -Standardfehlerkanal "stderr" (Workaround). - -2.4 Tail mit tmux - -Beim Verwenden des Parameters --tail werden automatisch relevante Log-Dateien angezeigt. -Der Befehl ist in Kombination mit dem Parameter --public-dns-name oder auch mit dem Szenario ---instance-with-load-balancer verwendbar. Zur Ausf�hrung muss das Skript in einer tmux-Session -ausgef�hrt werden. - -Beispiele: -./aws-setup.sh --tail --public-dns-name ec2-xxx.eu-west-2.compute.amazonaws.com -Zeigt relevante Log-Dateien der Instanz mit dem DNS Namen "ec2-xxx.eu-west-2.compute.amazonaws.com" an. - -./aws-setup.sh --tail --instance-with-load-balancer -Zeigt relevante Log-Dateien der Instanz die gerade erstellt wird, ab dem Zeitpunkt der Aufnahme der SSH-Verbindung - -3. TODO - -3.1 Fehlerbehebungen - -- Nutzer erstellen scheitert bisher mit "Precondition failed" (Nutzer- - name erf�llt Richtlinie nicht) - -3.2 Verbesserungen - -- run-instance Funktion nach "aws_functions.sh" faktorisieren -- Anpassung der Dateierzeugung an Linux ("crlf" ersetzen) -- Momentan wird der Admin-Nutzer als Eventersteller verwendet. - Die Rechtevergabe an einen anderen Nutzer �ber die REST-API - muss noch implementiert werden. - -3.2 Erweiterungen - -- list-Funktion mit Anzeige aller Instanznamen und deren DNS, Instanz-ID, etc. damit - --tail nicht die Web-GUI erfordert -- weitere Szenarios hinzuf�gen (Redeployment, R�ckmigration in Archiv, etc.) -- regionspezifisches Sourcing von Dateien mit Variablen (Auskommentieren nicht mehr notwendig) -- automatische Kontext�bernahme nach tmux - - -Getestet unter Microsoft Windows [Version 10.0.10586]: -- Cygwin 2.8.2(0.313/5/3) -- aws-cli/1.11.129 -- tmux 2.8.2(0.313/5/3) -- openssh 7.5p1-1 -- wget 1.19.1 -- curl 7.53.0 - -Funktioniert (Route53-Eintrag Erstellung konnte nicht getestet werden) - -Getestet unter Microsoft Windows [Version 6.3.96001]: -- Cygwin 2.9.0(0.318/5/3) -- aws-cli/1.11.166 -- tmux 2.4 -- openssh 7.4p1 -- wget 1.19.1 -- curl 7.55.1-1 - -Funktioniert (Route53-Eintrag Erstellung konnte nicht getestet werden) From 899ab82966c3ecd741aa96f65d0a17cf16304182 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 17:25:48 +0100 Subject: [PATCH 005/214] Fixed typo in README_en.txt --- configuration/aws-automation/README_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/README_en.txt b/configuration/aws-automation/README_en.txt index 8954b372f8e..81d1c5532cb 100644 --- a/configuration/aws-automation/README_en.txt +++ b/configuration/aws-automation/README_en.txt @@ -89,7 +89,7 @@ Miscellaneous auxiliary variables (time stamp, script name, etc.) lib/utils.sh: Functions for the output of colored messages, logging and sourcing of remaining bash files -1.2 Szenarios +1.2 Scenarios Scenarios are encapsulated execution units that are orchestrated by AWS and instance-specific functions to automate a specific use case. From e46f91a711515039dd25a02421e5c8633cc90c27 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 5 Dec 2017 17:40:36 +0100 Subject: [PATCH 006/214] Added description of aws_variables.sh inside README_en.txt --- configuration/aws-automation/README_en.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration/aws-automation/README_en.txt b/configuration/aws-automation/README_en.txt index 81d1c5532cb..372ac3bc0c1 100644 --- a/configuration/aws-automation/README_en.txt +++ b/configuration/aws-automation/README_en.txt @@ -89,6 +89,9 @@ Miscellaneous auxiliary variables (time stamp, script name, etc.) lib/utils.sh: Functions for the output of colored messages, logging and sourcing of remaining bash files +lib/variables_aws.sh: +Default values for variables + 1.2 Scenarios Scenarios are encapsulated execution units that are orchestrated by AWS and instance-specific functions From 049082b53d6c13f759e93a6c7989340f783d848f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Wed, 27 Dec 2017 04:55:35 +0100 Subject: [PATCH 007/214] Region specific sourcing. Sub instance scenario. Refactoring. --- configuration/aws-automation/README_en.txt | 15 +-- configuration/aws-automation/aws-setup.sh | 29 ++++- .../aws-automation/lib/config_eu_west_1.sh | 15 +++ .../aws-automation/lib/config_eu_west_2.sh | 15 +++ .../aws-automation/lib/functions_app.sh | 103 +++++++++------- .../aws-automation/lib/functions_ec2.sh | 65 +++------- .../aws-automation/lib/functions_elb.sh | 87 +++++++------ .../aws-automation/lib/functions_io.sh | 21 ++++ .../aws-automation/lib/functions_route53.sh | 8 +- .../aws-automation/lib/functions_wrapper.sh | 2 +- .../lib/scenario_associate_alb.sh | 23 ++-- .../lib/scenario_associate_clb.sh | 26 ++-- .../lib/scenario_associate_elastic_ip.sh | 20 ++- .../aws-automation/lib/scenario_instance.sh | 26 ++-- .../lib/scenario_sub_instance.sh | 116 ++++++++++++++++++ .../aws-automation/lib/util_functions.sh | 72 +++++++---- .../aws-automation/lib/util_variables.sh | 23 ---- configuration/aws-automation/lib/utils.sh | 6 + .../lib/{variables_aws.sh => variables.sh} | 45 +++---- 19 files changed, 449 insertions(+), 268 deletions(-) create mode 100644 configuration/aws-automation/lib/config_eu_west_1.sh create mode 100644 configuration/aws-automation/lib/config_eu_west_2.sh create mode 100644 configuration/aws-automation/lib/scenario_sub_instance.sh delete mode 100644 configuration/aws-automation/lib/util_variables.sh rename configuration/aws-automation/lib/{variables_aws.sh => variables.sh} (51%) diff --git a/configuration/aws-automation/README_en.txt b/configuration/aws-automation/README_en.txt index 372ac3bc0c1..2bb0f9a2c98 100644 --- a/configuration/aws-automation/README_en.txt +++ b/configuration/aws-automation/README_en.txt @@ -5,7 +5,7 @@ 0.2 Adjust default values 0.3 Show manual -1. Structure +1. Structure 1.1 Files and their tasks 1.2 Scenarios @@ -89,12 +89,9 @@ Miscellaneous auxiliary variables (time stamp, script name, etc.) lib/utils.sh: Functions for the output of colored messages, logging and sourcing of remaining bash files -lib/variables_aws.sh: -Default values for variables - 1.2 Scenarios -Scenarios are encapsulated execution units that are orchestrated by AWS and instance-specific functions +Scenarios are encapsulated execution units that are orchestrated by AWS and instance-specific functions to automate a specific use case. Scenarios include the following functions: @@ -109,9 +106,9 @@ Scenarios include the following functions: 2.1 Initialization of required variables Each scenario requires certain initialized variables for its execution. The assignment of a value to a variable takes place -either at the start of the script via the input of a parameter or if no parameter has been transferred, by input of the user after being prompted. +either at the start of the script via the input of a parameter or if no parameter has been transferred, by input of the user after being prompted. When the user is prompted to enter a value for a variable, he is being offered a default value out of the aws_variables.sh file. -This facilitates user input because certain values, such as the standard key name to connect to an instance don't change very often. +This facilitates user input because certain values, such as the standard key name to connect to an instance don't change very often. 2.2 Error handling @@ -119,13 +116,13 @@ To check the successful execution of a function, various strategies are used: 1. Checking the return value of a function -This strategy is applied to AWS-specific functions. When calling an AWS command (e.g. aws elb create-load-balancer) +This strategy is applied to AWS-specific functions. When calling an AWS command (e.g. aws elb create-load-balancer) the return value is either 0 (error occured) or 1 (no error occured). 2. Checking the HTTP status code When a curl command is called, the option -w "\ n% {http_code}" is also passed as a parameter. In doing so, response and HTTP code can be assigned -to two different variables, allowing separate processing. The HTTP status code can then be compared to the expected result, without loosing information +to two different variables, allowing separate processing. The HTTP status code can then be compared to the expected result, without loosing information about the actual response. 3. TODO diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 4c7abef6a1c..d27c8dc5915 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -17,6 +17,12 @@ else exit 1 fi +function source_region_config_file(){ + require_region + local region_config_fle=$(echo lib/config_$region.sh | tr '-' '_') + source $region_config_fle +} + # delete temp files when trapped function trapCleanup() { echo "" @@ -60,6 +66,8 @@ logFile="$HOME/Library/Logs/${scriptBasename}.log" function mainScript() { echo -n +source_region_config_file + if $tail; then check_if_tmux_is_used fi @@ -79,6 +87,12 @@ if $instance; then safeExit fi +if $sub_instance; then + sub_instance_start + confirm_reset_panes + safeExit +fi + if $tail ; then tail_start confirm_reset_panes @@ -105,17 +119,22 @@ usage() { -l, --instance-short-name Short name for instance (e.g. subdomain \"wcs17\") -a, --new-admin-password New password for the admin user -p, --public-dns-name Dns name of instance (e.g. \"ec2-35-176...amazonaws.com\") + -p, --super-instance Dns name of superior instance (e.g. base instance for sub instances) + -w, --description Description of sub instance + -c, --contact-person Contact person + -e, --contact-email Email of contact person -f, --force Skip user input and use default variables -d, --debug Debug mode ${bold}Scenarios:${reset} --instance Create instance + --sub-instance Create sub instance --associate-alb Associate instance with existing application load balancer whos listener is defined in variables_aws.sh. Automatically create necessary target group and host name rule. Currently *.dummy.sapsailing.com is used for test purposes. --associate-clb Associate instance with new classic load balancer. - --asslociate-elastic-ip Associate instance with new elastic ip. + --associate-elastic-ip Associate instance with new elastic ip. --tail Tail logs from instance using tmux ${bold}Other:${reset} @@ -204,6 +223,7 @@ associate_clb=false associate_alb=false associate_elastic_ip=false instance=false +sub_instance=false tail=false # Read the options and set variables @@ -222,12 +242,17 @@ while [[ $1 = -?* ]]; do -l|--instance-short-name) shift; instance_short_name_param=${1} ;; -a|--new-admin-password) shift; new_admin_password_param=${1} ;; -p|--public-dns-name) shift; public_dns_name_param=${1} ;; + -b|--super-instance) shift; super_instance_param=${1} ;; + -w|--description) shift; description_param=${1} ;; + -c|--contact-person) shift; contact_person_param=${1} ;; + -e|--contact-email) shift; contact_email_param=${1} ;; -f|--force) force=true ;; -d|--debug) debug=true ;; + --instance) instance=true ;; + --sub-instance) sub_instance=true ;; --associate-clb) associate_clb=true ;; --associate-alb) associate_alb=true ;; --associate-elastic-ip) associate_elastic_ip=true ;; - --instance) instance=true ;; --tail) tail=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh new file mode 100644 index 00000000000..cec3d646977 --- /dev/null +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -0,0 +1,15 @@ +# Variables for region "eu-west-1" (Ireland) + +# HTTPS Listener of Application Load Balancer Sailing-eu-west-1 +listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/Sailing-eu-west-1/32b89dbfe7f75097/f9212223209ac042' + +# Sailing Analytics App +instance_security_group_ids=sg-eaf31e85 + +# SAP Sailing Analytics App HVM 1.59 +image_id='ami-407ef339' + +certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' +elb_security_group_ids= +mongodb_host=54.76.64.42 +mongodb_port=27017 diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh new file mode 100644 index 00000000000..0faa1402a4c --- /dev/null +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -0,0 +1,15 @@ +# Variables for region "eu-west-2" (London) + +# HTTPS Listener of Application Load Balancer DummyALB +listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' + +# Sailing Analytics App +instance_security_group_ids=sg-871732ee + +# SAP Sailing Analytics App HVM 1.56 +image_id=ami-39f3e25d + +certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' +elb_security_group_ids=sg-871732ee +mongodb_host=35.176.143.232 +mongodb_port=27017 diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index f2e26ddc68b..79fbd01c3be 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -22,52 +22,33 @@ function get_latest_release(){ echo "$result" | head -1 } -# ----------------------------------------------------------- -# Get access token -# @param $1 admin username -# @param $2 admin password -# @param $3 dns name of instance -# @return access token -# ----------------------------------------------------------- -function get_access_token(){ - local_echo "Getting access token..." - curl_wrapper -X GET "http://$1:$2@$3:8888/security/api/restsecurity/access_token" | get_attribute '.access_token' -} - -# ----------------------------------------------------------- -# Creates a new event with event name = instance name, no regatta and venuename="Default" -# @param $1 access token of privileged user -# @param $2 dns name of instance -# @param $3 event name -# @return event_id of created event -# ----------------------------------------------------------- -function create_event(){ - local_echo "Creating event with name $3..." - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:8888/sailingserver/api/v1/events/createEvent" --data "eventName=$3" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' -} # ----------------------------------------------------------- # Changes password of user # @param $1 access token of privileged user # @param $2 dns name of instance -# @param $3 admin username -# @param $4 admin new password +# @param $3 dns name of instance +# @param $4 admin username +# @param $5 admin new password # ----------------------------------------------------------- function change_admin_password(){ - local_echo "Changing password of user $3 to $4..." - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:8888/security/api/restsecurity/change_password" --data "username=$3" --data "password=$4" + local_echo "Changing password of user $4 to $5..." + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/change_password" \ + --data "username=$4" --data "password=$5" } # ----------------------------------------------------------- # Creates new user # @param $1 access token of privileged user # @param $2 dns name of instance -# @param $3 user username -# @param $4 user password +# @param $3 port +# @param $4 user username +# @param $5 user password # ----------------------------------------------------------- function create_new_user(){ - local_echo "Creating new user \"$3\" with password \"$4\"..." - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:8888/security/api/restsecurity/create_user" --data "username=$3" --data "password=$4" + local_echo "Creating new user \"$4\" with password \"$5\"..." + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/create_user" \ + --data "username=$4" --data "password=$5" } # ----------------------------------------------------------- @@ -75,42 +56,76 @@ function create_new_user(){ # @param $1 admin username # @param $2 admin password # @param $3 public_dns_name +# @param $4 port # ----------------------------------------------------------- function wait_for_access_token_resource(){ - echo -n "Wait until resource \"/security/api/restsecurity/access_token\" is available..." - curl_until_http_200 http://$1:$2@$3:8888/security/api/restsecurity/access_token + local_echo -n "Wait until resource \"/security/api/restsecurity/access_token\" is available..." + curl_until_http_200 http://$1:$2@$3:$4/security/api/restsecurity/access_token } +# ----------------------------------------------------------- +# Get access token +# @param $1 admin username +# @param $2 admin password +# @param $3 dns name of instance +# @param $4 port +# @return access token +# ----------------------------------------------------------- +function get_access_token(){ + local_echo "Getting access token..." + wait_for_access_token_resource $1 $2 $3 $4 1>&2 + curl_wrapper -X GET "http://$1:$2@$3:$4/security/api/restsecurity/access_token" | get_attribute '.access_token' +} + + # ----------------------------------------------------------- # Wait until create event resource is available # @param $1 public_dns_name +# @param $2 port # ----------------------------------------------------------- function wait_for_create_event_resource(){ - echo -n 'Wait until resource "/sailingserver/api/v1/events/createEvent" is available...' - curl_until_http_401 http://$1:8888/sailingserver/api/v1/events/createEvent + local_echo -n 'Wait until resource "/sailingserver/api/v1/events/createEvent" is available...' + curl_until_http_401 http://$1:$2/sailingserver/api/v1/events/createEvent } # ----------------------------------------------------------- -# Patch 001-events.conf with ssl macros +# Creates a new event with event name = instance name, no regatta and venuename="Default" +# @param $1 access token of privileged user +# @param $2 dns name of instance +# @param $3 port +# @param $4 event name +# @return event_id of created event +# ----------------------------------------------------------- +function create_event(){ + local_echo "Creating event with name $4..." + wait_for_create_event_resource $2 $3 1>&2 + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/sailingserver/api/v1/events/createEvent" \ + --data "eventName=$4" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' +} + +# ----------------------------------------------------------- +# Patch 001-events.conf with Even-SSL macro # @param $1 dns name # @param $2 event id # @param $3 ssh user # @param $4 public dns name -# @param $5 use ssl +# @param $5 port # ----------------------------------------------------------- -function configure_apache(){ - wait_for_ssh_connection "$3" "$4" - wait_for_001_events_patch "$3" "$4" - local patched_content="Use Event-SSL $1 \"$2\" 127.0.0.1 8888" - ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" - #ssh_wraper $3@$4 "/etc/init.d/httpd reload" +function append_event_ssl_macro_to_001_events_conf(){ + wait_for_ssh_connection $3 $4 + wait_for_001_events_patch $3 $4 + ssh_wrapper $3@$4 "echo -e \"Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5\" >> $events_conf" + ssh_wrapper $3@$4 "/etc/init.d/httpd reload" } +# ----------------------------------------------------------- +# Waits till 001-events.conf exists on the server +# ----------------------------------------------------------- function wait_for_001_events_patch(){ echo -n "Waiting for 001-events.conf to be created..." while [[ $(ssh_wrapper $1@$2 test -f $events_conf && echo "ok") != "ok" ]]; do echo -n . sleep 2 done - success "001-events.conf now exists. Appending macros to overwrite sailing script" + success "001-events.conf now exists. Appending macros..." } diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 0f48cbb2898..861d7641ed4 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -7,7 +7,7 @@ # ----------------------------------------------------------- function query_public_dns_name(){ local_echo "Querying for the instance public dns name..." - aws_wrapper --region "$region" ec2 describe-instances --instance-ids "$1" --output text --query 'Reservations[*].Instances[*].PublicDnsName' + aws_wrapper ec2 describe-instances --instance-ids $1 --output text --query 'Reservations[*].Instances[*].PublicDnsName' } # ----------------------------------------------------------- @@ -19,6 +19,16 @@ function get_default_vpc_id(){ aws_wrapper ec2 describe-vpcs --query 'Vpcs[?IsDefault==`true`].VpcId' --output text } +# ----------------------------------------------------------- +# Query for instance id by public dns name +# param $1 public dns name +# @return instance id +# ----------------------------------------------------------- +function get_instance_id(){ + local_echo "Querying for the instance id..." + aws_wrapper aws ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text +} + # ----------------------------------------------------------- # Allocates an elastic ip address # @return elastic ip @@ -35,7 +45,7 @@ function allocate_address(){ # ----------------------------------------------------------- function associate_address(){ local_echo "Associating instance \"$1\" with elastic ip \"$2\"..." - aws_wrapper ec2 associate-address --instance-id "$1" --public-ip "$2" + aws_wrapper ec2 associate-address --instance-id $1 --public-ip $2 } # ----------------------------------------------------------- @@ -43,7 +53,7 @@ function associate_address(){ # @return availability zones array # ----------------------------------------------------------- function get_availability_zones(){ - aws_wrapper ec2 --region $region describe-availability-zones --query "AvailabilityZones[].ZoneName" + aws_wrapper ec2 describe-availability-zones --query "AvailabilityZones[].ZoneName" } # ----------------------------------------------------------- @@ -70,51 +80,14 @@ function wait_for_ssh_connection(){ } -# ----------------------------------------------------------- -# Creates $count instances inside $region with image $image_id and -# $instance_type and $user_data as well as according $tag_specifications, -# $security_group_ids and $key_name -# @return instance id -# ----------------------------------------------------------- function run_instance(){ local_echo "Creating instance..." - write_user_data_to_file - local command="aws_wrapper --region $region ec2 run-instances" - command+=$(add_param "region" $region) - command+=$(add_param "image-id" $image_id) - command+=$(add_param "count" $instance_count) - command+=$(add_param "instance-type" $instance_type) - command+=$(add_param "key-name" $key_name) - command+=$(add_param "security-group-ids" $instance_security_group_ids) - command+=$(add_param "user-data" 'file://${tmpDir}/$user_data_file') - command+=$(add_param "tag-specifications" $(printf $tag_specifications "$instance_name")) + local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ + "INSTALL_FROM_RELEASE=$(get_latest_release)" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - eval "$command" -} - -# ----------------------------------------------------------- -# Creates a file with user data for instance creation -# ----------------------------------------------------------- -function write_user_data_to_file(){ - local MONGODB_HOST="$mongodb_host" - local MONGODB_PORT="$mongodb_port" - local MONGODB_NAME="$(lower_trim $instance_name | only_letters_and_numbers)" - local REPLICATION_CHANNEL="$MONGODB_NAME" - local SERVER_NAME="$MONGODB_NAME" - local USE_ENVIRONMENT="live-server" - local INSTALL_FROM_RELEASE="$(get_latest_release)" - local SERVER_STARTUP_NOTIFY="$default_server_startup_notify" - - local content= - content+=$(add_user_data_variable "MONGODB_HOST" $MONGODB_HOST) - content+=$(add_user_data_variable "MONGODB_PORT" $MONGODB_PORT) - content+=$(add_user_data_variable "MONGODB_NAME" $MONGODB_NAME) - content+=$(add_user_data_variable "INSTALL_FROM_RELEASE" $INSTALL_FROM_RELEASE) - content+=$(add_user_data_variable "USE_ENVIRONMENT" $USE_ENVIRONMENT) - content+=$(add_user_data_variable "REPLICATION_CHANNEL" $REPLICATION_CHANNEL) - content+=$(add_user_data_variable "SERVER_NAME" $SERVER_NAME) - content+=$(add_user_data_variable "SERVER_STARTUP_NOTIFY" $SERVER_STARTUP_NOTIFY) - - echo "$content" > "${tmpDir}/$user_data_file" + aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ + --security-group-ids $instance_security_group_ids --user-data "$user_data" \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" } diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index ae9e1711bf3..a03826fa256 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash - # ----------------------------------------------------------- # Create target group for load balancer # @param $1 target group name @@ -8,8 +7,35 @@ # ----------------------------------------------------------- function create_target_group(){ local_echo "Creating target group..." - local target_group_name=$(echo "$1" | only_letters_and_numbers) - aws_wrapper elbv2 create-target-group --name $target_group_name --protocol HTTPS --port 443 --vpc-id "$2" + local target_group_name=$(alphanumeric $1) + local vpc_id=$(get_default_vpc_id) + aws_wrapper elbv2 create-target-group --name $target_group_name --protocol $target_group_protocol \ + --port $target_group_port --vpc-id $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn' +} + +# ----------------------------------------------------------- +# Sets health check of target group +# @param $1 target group arn +# @param $2 health check protocol (e.g. "HTTP") +# @param $3 health check path (e.g. "/index.html") +# @param $4 health check interval seconds (e.g. "5") +# @param $5 health check timeouts seconds (e.g. "4") +# @param $6 healthy threshold count (e.g. "2") +# @param $7 unhealthy threshold count (e.g. "2") +# ----------------------------------------------------------- +function set_target_group_health_check(){ + aws_wrapper elbv2 modify-target-group --target-group-arn $1 --health-check-protocol $2 --health-check-path $3 \ + --health-check-interval-seconds $4 --health-check-timeout-seconds $5 --healthy-threshold-count $6 --unhealthy-threshold-count $7 +} + +# ----------------------------------------------------------- +# Add tag to target group +# @param $1 target group arn +# @param $2 key (e.g. "Description") +# @param $2 value (e.g. "This is an instance for ...") +# ----------------------------------------------------------- +function set_target_group_tag(){ + aws_wrapper elbv2 add-tags --resource-arns $1 --tags "Key=$2,Value=$3" } # ----------------------------------------------------------- @@ -19,7 +45,7 @@ function create_target_group(){ # ----------------------------------------------------------- function register_targets(){ local_echo "Register instance within target group..." - aws elbv2 register-targets --target-group-arn "$1" --targets Id="$2" + aws elbv2 register-targets --target-group-arn $1 --targets Id=$2 } # ----------------------------------------------------------- @@ -31,11 +57,12 @@ function register_targets(){ # ----------------------------------------------------------- function create_rule(){ local_echo "Creating rule for listener..." - local subdomain=$(echo "$2" | only_letters_and_numbers) + local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.dummy.sapsailing.com" - aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority --conditions Field=host-header,Values="$domain" --actions Type=forward,TargetGroupArn=$3 1>&2 - echo "$domain" + local domain="$subdomain.dummy2.sapsailing.com" + aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ + --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3 1>&2 + echo $domain } # ----------------------------------------------------------- @@ -44,7 +71,8 @@ function create_rule(){ # @return highest priority # ----------------------------------------------------------- function get_rule_with_highest_priority(){ - local max_priority=$(aws_wrapper elbv2 describe-rules --listener-arn $1 --query "sort_by(Rules, &Priority)[*].{P: Priority} | [-2:-1]" --output text) + local max_priority=$(aws_wrapper elbv2 describe-rules --listener-arn $1 \ + --query "sort_by(Rules, &Priority)[*].{P: Priority} | [-2:-1]" --output text) if is_number $max_priority; then echo $max_priority else @@ -52,17 +80,6 @@ function get_rule_with_highest_priority(){ fi } -# ----------------------------------------------------------- -# Create load balancer with HTTP rule -# @param $1 load_balancer_name -# @return json result -# ----------------------------------------------------------- -function create_load_balancer_http(){ - local load_balancer_name=$(echo "$1" | only_letters_and_numbers) - local_echo "Creating load balancer \"$load_balancer_name\"..." - aws_wrapper elb create-load-balancer --load-balancer-name $load_balancer_name --listeners "Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80" --availability-zones "$(get_availability_zones)" --security-groups "$elb_security_group_ids" -} - # NOT TESTED # ----------------------------------------------------------- # Create load balancer with HTTPS rule @@ -72,9 +89,12 @@ function create_load_balancer_http(){ # @return json result # ----------------------------------------------------------- function create_load_balancer_https(){ - local load_balancer_name=$(echo "$1" | only_letters_and_numbers) local_echo "Creating load balancer \"$load_balancer_name\"..." - aws_wrapper elb create-load-balancer --load-balancer-name $load_balancer_name --listeners "Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80" "Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=$2" --availability-zones "$(get_availability_zones)" --security-groups "$elb_security_group_ids" + local load_balancer_name=$(alphanumeric $1) + aws_wrapper elb create-load-balancer --load-balancer-name $load_balancer_name \ + --listeners "Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80" \ + "Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=$2" \ + --availability-zones "$(get_availability_zones)" --security-groups "$elb_security_group_ids" } # ----------------------------------------------------------- @@ -83,21 +103,10 @@ function create_load_balancer_https(){ # @return json result # ----------------------------------------------------------- function configure_health_check_http(){ - local load_balancer_name=$(echo "$1" | only_letters_and_numbers) local_echo "Configuring load balancer health check..." - aws_wrapper elb configure-health-check --load-balancer-name "$load_balancer_name" --health-check Target=TCP:8888,Interval=15,UnhealthyThreshold=2,HealthyThreshold=3,Timeout=5 -} - -# NOT TESTED -# ----------------------------------------------------------- -# Configure health check HTTPS -# @param $1 load balancer name -# @return json result -# ----------------------------------------------------------- -function configure_health_check_https(){ - local_echo "Configuring load balancer health check..." - local load_balancer_name=$(echo "$1" | only_letters_and_numbers) - aws_wrapper elb configure-health-check --load-balancer-name "$load_balancer_name" --health-check Target=HTTPS:443/index.html,Interval=15,UnhealthyThreshold=2,HealthyThreshold=3,Timeout=5 + local load_balancer_name=$(alphanumeric $1) + aws_wrapper elb configure-health-check --load-balancer-name $load_balancer_name \ + --health-check Target=HTTP:80,Interval=5,UnhealthyThreshold=2,HealthyThreshold=2,Timeout=4 } # ----------------------------------------------------------- @@ -106,8 +115,8 @@ function configure_health_check_https(){ # @param $2 instance id # @return json result # ----------------------------------------------------------- -function add_instance_to_elb(){ - local_echo "Adding instance to elb..." - local load_balancer_name=$(echo "$1" | only_letters_and_numbers) +function add_instance_to_clb(){ + local_echo "Adding instance to clb..." + local load_balancer_name=$(alphanumeric $1) aws_wrapper elb register-instances-with-load-balancer --load-balancer-name $load_balancer_name --instances $2 } diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index bcf02765e83..743fc0526c8 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -104,6 +104,23 @@ function require_ssh_user(){ require_variable "$ssh_user_param" ssh_user "" "$ssh_user_ask_message" } +function require_super_instance(){ + require_variable "$super_instance_param" super_instance "" "$super_instance_message" +} + +function require_description(){ + require_variable "$description_param" description "$instance_name" "$description_message" +} + +function require_contact_person(){ + require_variable "$contact_person_param" contact_person "" "$contact_person_message" +} + +function require_contact_email(){ + require_variable "$contact_email_param" contact_email "" "$contact_email_message" +} + + region_ask_message="Please enter the region for the instance: " instance_type_ask_message="Please enter the instance type: " key_name_ask_message="Please enter the name of your keypair to connect to the instance: " @@ -117,3 +134,7 @@ user_username_ask_message="Please enter the username of your new user: " user_password_ask_message="Please enter the password of your new user: " public_dns_name_ask_message="Please enter the public dns name: " ssh_user_ask_message="Please enter the ssh user: " +super_instance_message="Please enter the dns name of the superior instance: " +description_message="Please enter a description for the server: " +contact_person_message="Please enter the name of a contact person: " +contact_email_message="Please enter the email of the contact person: " diff --git a/configuration/aws-automation/lib/functions_route53.sh b/configuration/aws-automation/lib/functions_route53.sh index e8a7e37f28a..8abd9aac8cb 100644 --- a/configuration/aws-automation/lib/functions_route53.sh +++ b/configuration/aws-automation/lib/functions_route53.sh @@ -10,9 +10,10 @@ # @return json result # ----------------------------------------------------------- function route53_change_resource_record(){ - create_route53_record_file "$1" "$2" "$3" + create_route53_record_file $1 $2 $3 local_echo "Creating Route53 record set (Name: $1.sapsailing.com Value: $3 Type: CNAME)..." - aws_wrapper route53 change-resource-record-sets --hosted-zone-id "$hosted_zone_id" --change-batch "file://${tmpDir}/$change_resource_record_set_file" + aws_wrapper route53 change-resource-record-sets --hosted-zone-id "$hosted_zone_id" \ + --change-batch "file://${tmpDir}/$change_resource_record_set_file" } # ----------------------------------------------------------- @@ -22,6 +23,7 @@ function route53_change_resource_record(){ # @param $3 load balancer dns name # ----------------------------------------------------------- function create_route53_record_file(){ - local json=$(printf '{"Changes":[{"Action":"CREATE","ResourceRecordSet": {"Name": "%s.sapsailing.com","Type": "CNAME","TTL": %s,"ResourceRecords":[{"Value": "%s"}]}}]}' $1 $2 $3) + local json=$(printf '{"Changes":[{"Action":"CREATE","ResourceRecordSet": \ + {"Name": "%s.sapsailing.com","Type": "CNAME","TTL": %s,"ResourceRecords":[{"Value": "%s"}]}}]}' $1 $2 $3) echo "$json" > "${tmpDir}/$change_resource_record_set_file" } diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index fcd2c18e6bf..7838d76df25 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -33,7 +33,7 @@ function curl_wrapper(){ # ----------------------------------------------------------- function aws_wrapper(){ local out; - out=$(aws "$@") + out=$(aws --region $region "$@") if command_was_successful $?; then success "[ OK ]" echo $out | sanitize diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index 59a5345e566..9ac4186dab0 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -14,23 +14,22 @@ function associate_alb_start(){ # the user will be prompted to enter a value # ----------------------------------------------------------- function associate_alb_require(){ - require_region - require_instance_type - require_instance_name - require_instance_short_name - require_key_name - require_key_file - require_new_admin_password - require_user_username - require_user_password + # will only be executed after instance creation + : } function associate_alb_execute() { - local vpc_id=$(get_default_vpc_id) - local target_group_arn=$(create_target_group $instance_name $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn') + # create target group for instance + local target_group_arn=$(create_target_group $instance_name) + + # add instance to target group register_targets $target_group_arn $instance_id + + # create rule for host-based forwarding within alb local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) header "Apache configuration" - configure_apache "$domain" "$event_id" "$ssh_user" "$public_dns_name" "ssl" + + # configure apache with event macro + append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $public_dns_name "8888" } diff --git a/configuration/aws-automation/lib/scenario_associate_clb.sh b/configuration/aws-automation/lib/scenario_associate_clb.sh index ba1932b307d..65146cde589 100644 --- a/configuration/aws-automation/lib/scenario_associate_clb.sh +++ b/configuration/aws-automation/lib/scenario_associate_clb.sh @@ -14,24 +14,24 @@ function associate_clb_start(){ # the user will be prompted to enter a value # ----------------------------------------------------------- function associate_clb_require(){ - require_region - require_instance_type - require_instance_name - require_instance_short_name - require_key_name - require_key_file - require_new_admin_password - require_user_username - require_user_password + # will only be executed after instance creation + : } function associate_clb_execute() { header "Associating classic load balancer" - local load_balancer_dns_name=$(create_load_balancer_https "$instance_name" "$certificate_arn" | get_attribute '.DNSName') - local json_health_check=$(configure_health_check_https "$instance_name") - local json_added_instance_response=$(add_instance_to_elb "$instance_name" "$instance_id") + # create classic load balancer + local load_balancer_dns_name=$(create_load_balancer_https $instance_name $certificate_arn | get_attribute '.DNSName') + + # configure health check of load balancer + local json_health_check=$(configure_health_check_https $instance_name) + + # add instance to clb + local json_added_instance_response=$(add_instance_to_clb $instance_name $instance_id) header "Apache configuration" - configure_apache "$public_dns_name" "$event_id" "$ssh_user" "$public_dns_name" "ssl" + + # configure apache with event macro + append_event_ssl_macro_to_001_events_conf $public_dns_name $event_id $ssh_user $public_dns_name '8888' } diff --git a/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh b/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh index 554bd280db3..2ced373d8d9 100644 --- a/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh +++ b/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh @@ -14,23 +14,21 @@ function associate_elastic_ip_start(){ # the user will be prompted to enter a value # ----------------------------------------------------------- function associate_elastic_ip_require(){ - require_region - require_instance_type - require_instance_name - require_instance_short_name - require_key_name - require_key_file - require_new_admin_password - require_user_username - require_user_password +# will only be executed after instance creation + : } function associate_elastic_ip_execute() { header "Elastic ip creation" + # allocate elastic ip local elastic_ip=$(allocate_address) - associate_address "$instance_id" "$elastic_ip" + + # associate elastic ip + associate_address $instance_id $elastic_ip header "Apache configuration" - configure_apache "$elastic_ip" "$event_id" "$ssh_user" "$elastic_ip" "ssl" + + # configure apache with event macro + append_event_ssl_macro_to_001_events_conf $elastic_ip $event_id $ssh_user $elastic_ip '8888' } diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 95b9e7a5c0a..cae6076c5a8 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -28,30 +28,26 @@ function instance_require(){ function instance_execute() { header "Instance Initialization" + # create instance local json_instance=$(run_instance) instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') - instance_private_ip=$(echo "$json_instance" | get_attribute '.Instances[0].PrivateIpAddress') - wait_instance_exists "$instance_id" + # wait till instance is recognized by aws + wait_instance_exists $instance_id - public_dns_name=$(query_public_dns_name "$instance_id") - - header "SSH Connection" - - wait_for_ssh_connection "$ssh_user" "$public_dns_name" + # get public dns name of instance + public_dns_name=$(query_public_dns_name $instance_id) + # if --tail option is passed then tail logfiles of instance within tmux if $tail; then tail_start fi header "Event and user creation" - wait_for_access_token_resource "$admin_username" "$admin_password" "$public_dns_name" - access_token=$(get_access_token "$admin_username" "$admin_password" "$public_dns_name") - - wait_for_create_event_resource "$public_dns_name" - event_id=$(create_event "$access_token" "$public_dns_name" "$instance_name") - - change_admin_password "$access_token" "$public_dns_name" "$admin_username" "$new_admin_password" - create_new_user "$access_token" "$public_dns_name" "$user_username" "$user_password" + local port="8888" + access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) + event_id=$(create_event $access_token $public_dns_name $port $instance_name) + change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password + create_new_user $access_token $public_dns_name $port $user_username $user_password } diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh new file mode 100644 index 00000000000..765dcf45a75 --- /dev/null +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +# Scenario for creating a sub instance +# ------------------------------------------------------ + +function sub_instance_start(){ + sub_instance_require + sub_instance_execute +} + +# ----------------------------------------------------------- +# All these variables are needed for this scenario +# If one variable is not assigned or passed by parameter +# the user will be prompted to enter a value +# ----------------------------------------------------------- +function sub_instance_require(){ + require_super_instance + + require_key_name + require_key_file + require_instance_name + require_instance_short_name + require_new_admin_password + require_user_username + require_user_password + + # require_description + # require_contact_person + # require_contact_email +} + +function sub_instance_execute() { + local servers_dir="/home/sailing/servers/" + local server_dir="$servers_dir$instance_short_name" + local server_env_file="$server_dir/env.sh" + local comment_out_line_in_env_with_pattern='JAVA_HOME=\/opt\/jdk1.8.0_20' + local comment_in_line_in_env_with_pattern='sapjvm_gc' + + # create sub instance server directory + execute_remote mkdir $server_dir + + # copy refreshInstance.sh from /servers/server to sub instance directory + execute_remote cp /home/sailing/servers/server/refreshInstance.sh $server_dir + + # Execute refreshInstance.sh + local latest_release=$(get_latest_release) + execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $latest_release;" + + # uncommenting lines containing pattern + execute_remote "sed -i '/$comment_in_line_in_env_with_pattern/s/^#//g' $server_env_file" + + # commenting out lines containing pattern + execute_remote "sed -i '/$comment_out_line_in_env_with_pattern/s/^/#/g' $server_env_file" + + local server_port=$(find_first_unused_number_for_variable "SERVER_PORT" "8880") + local telnet_port=$(find_first_unused_number_for_variable "TELNET_PORT" "14900") + local expedition_port=$(find_first_unused_number_for_variable "EXPEDITION_PORT" "2000") + + # TODO: Append description to README + + local patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ + "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$mongodb_host" \ + "MONGODB_PORT=$mongodb_port" "DEPLOY_TO=$instance_short_name") + + # append patch to env.sh + execute_remote "echo -e \"$patch\" >> $server_env_file" + + # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. + execute_remote -f "sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" + + # get access token + access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) + + # create event + event_id=$(create_event $access_token $super_instance $server_port $instance_name) + + # change admin password + change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password + + # create new user + create_new_user $access_token $super_instance $server_port $user_username $user_password + + # TODO: handle situation if super instance is not inside application load balancer + + local target_group_arn=$(create_target_group $instance_name) + register_targets $target_group_arn $(get_instance_id $super_instance) + + local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) + + append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + + # TODO: write email +} + +# ----------------------------------------------------------- +# For a given variable name, find first number that is not already used within all env.sh files inside /home/sailing/servers. +# If no number is found, return $2 +# @param $1 variable name (e.g. SERVER_PORT) +# @param $2 default number (e.g. 8888) +# ----------------------------------------------------------- +function find_first_unused_number_for_variable(){ + numbers=($(find_all_distinct_values_of_variable_inside_env_files "$1")) + if [ ${#numbers[@]} -eq 0 ]; then + echo "$2" + else + find_first_missing_number_in_array "${numbers[@]}" + fi +} + +function execute_remote(){ + ssh_wrapper $ssh_user@$super_instance "$@" +} + +function find_all_distinct_values_of_variable_inside_env_files(){ + execute_remote grep -Roh --include=env.sh "$1=.*" $servers_dir | tr -d "$1=" | sort | uniq +} diff --git a/configuration/aws-automation/lib/util_functions.sh b/configuration/aws-automation/lib/util_functions.sh index 3168b45d9e0..99c736a547a 100644 --- a/configuration/aws-automation/lib/util_functions.sh +++ b/configuration/aws-automation/lib/util_functions.sh @@ -1,37 +1,42 @@ #!/usr/bin/env bash # ----------------------------------------------------------- -# Creates a parameter from a key and value -# @param $1 key -# @param $2 value -# @return result ("--key value") +# Appends multiple parameters (e.g. "SERVER_NAME=Test") +# Also includes parameters starting with "#" as comments. +# @param $@ variable key and value (e.g. "MONGODB_HOST=123.123.123.123") # ----------------------------------------------------------- -function add_param() { - if [ ! -z "$2" ]; then - local result=" --$1 $2" - fi - echo "$result" -} - -# ----------------------------------------------------------- -# Constructs string of user data variable name and value plus linebreak -# @param $1 user data variable name -# @param $2 user data variable value -# @return name=value (linebreak) if value not empty else nothing -# ----------------------------------------------------------- -function add_user_data_variable(){ - if ! [ -z "$2" ]; then - local CR_LF=$'\r'$'\n' - local content="$1=$2" - content+=$CR_LF - echo "$content" +function build_configuration(){ + for var in "$@" + do + # if parameters starts with #, add parameters to content + if [[ $var == \#* ]]; then + content+="$var\n" + continue fi + + key=${var%=*} + value=${var#*=} + + if [[ ! -z "$key" && ! -z "$value" ]]; then + content+="$key=$value\n" + fi + done + echo -e $content } function command_was_successful(){ [ $1 -eq 0 ] } +# ----------------------------------------------------------- +# Uncomments a line starting with a specific pattern +# @param $1 pattern +# @param $2 file +# ----------------------------------------------------------- +function uncomment_line_starting_with(){ + sed -i '/$1/s/^#//g' $2 +} + # ----------------------------------------------------------- # Checks if variable is a number # @param $1 returnvariablevalue @@ -41,6 +46,23 @@ function is_number(){ [[ $1 =~ ^-?[0-9]+$ ]] } +# ----------------------------------------------------------- +# Finds first missing number in array of sorted numbers +# @param $1 array of numbers +# @return first missing number +# ----------------------------------------------------------- +function find_first_missing_number_in_array(){ + arr=("$@") + for (( i = 0 ; i < $((${#arr[@]})) ; i++ )); do + if [ "$((${arr[$i]}+1))" != "${arr[$(($i + 1))]}" ] ;then + missing_number="$((${arr[$i]}+1))" + break + fi + done + echo $missing_number +} + + # ----------------------------------------------------------- # Check if variable is a number and its value is 200 # @param $1 variable @@ -66,6 +88,10 @@ function sanitize(){ tr -d '\r' } +function alphanumeric(){ + lower_trim $1 | only_letters_and_numbers +} + # ------------------------------------------------------ # The following functions were part of a bash template # and could be useful in the future diff --git a/configuration/aws-automation/lib/util_variables.sh b/configuration/aws-automation/lib/util_variables.sh deleted file mode 100644 index 2c02cadd048..00000000000 --- a/configuration/aws-automation/lib/util_variables.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# ------------------------------------------------------ -# The following variables were part of a bash template -# and could be useful in the future -# ------------------------------------------------------ - -# SCRIPTNAME -scriptName=`basename $0` #Set Script Name variable -scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName - -# TIMESTAMPS - -now=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 -datestamp=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 -hourstamp=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM -timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 -today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 -longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 -gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT - -# THISHOST -thisHost=$(hostname) diff --git a/configuration/aws-automation/lib/utils.sh b/configuration/aws-automation/lib/utils.sh index f6e44d89b48..8b9d0533a58 100644 --- a/configuration/aws-automation/lib/utils.sh +++ b/configuration/aws-automation/lib/utils.sh @@ -86,6 +86,12 @@ do if [[ "${utility_file}" == *"utils.sh"* ]]; then continue fi + + # Don't source config files for now + if [[ "${utility_file}" == *"config_"* ]]; then + continue + fi + source "$utility_file" fi done diff --git a/configuration/aws-automation/lib/variables_aws.sh b/configuration/aws-automation/lib/variables.sh similarity index 51% rename from configuration/aws-automation/lib/variables_aws.sh rename to configuration/aws-automation/lib/variables.sh index 6febc055f58..9d2a64ad2f1 100644 --- a/configuration/aws-automation/lib/variables_aws.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -1,12 +1,22 @@ #!/usr/bin/env bash -# Set variables here -# ------------------------------------------------------ +# SCRIPTNAME +scriptName=`basename $0` #Set Script Name variable +scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName -# Please comment out the variables of the region you are not using +# TIMESTAMPS +declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 +datestamp=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 +hourstamp=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM +timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 +today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 +longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 +gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT + +# THISHOST +thisHost=$(hostname) # Default region unspecific variables - default_region=eu-west-2 default_key_name=leonradeck-keypair default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' @@ -17,38 +27,19 @@ default_user_username=newuser default_user_password=newpassword hosted_zone_id=Z1R8UBAEXAMPLE -# Variables for region "eu-west-2" (London) - -instance_security_group_ids=sg-871732ee -image_id=ami-39f3e25d -certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' -elb_security_group_ids=sg-871732ee -mongodb_host=35.176.143.232 -mongodb_port=27017 -listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/22ab628fba30d0e7' - -# Variables for region "eu-west-1" (Ireland) - -# instance_security_group_ids=sg-eaf31e85 -# image_id= -# certificate_arn='' -# elb_security_group_ids= -# mongodb_host=54.76.64.42 -# mongodb_port=27017 - - # Other Variables - +target_group_protocol='HTTPS' +target_group_port='443' ssh_retry_interval=2 http_retry_interval=5 -user_data_file=".userdata.txt" +env_file=".env.txt" change_resource_record_set_file=".change-resource-record-set.json" tag_specifications="\'ResourceType=instance,Tags=[{Key=Name,Value=%s}]\'" admin_username=admin admin_password=admin instance_count=1 ssh_user=root -default_instance_name="$now" +default_instance_name="$script_start_time" default_instance_short_name="$timestamp" sailing_0='/home/sailing/servers/server/logs/sailing0.log.0' sailing_err='/var/log/sailing.err' From f58cc0a5581dc9df0057cc40190357de214350f2 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 29 Dec 2017 18:21:41 +0100 Subject: [PATCH 008/214] bug 1301: started with a WindFinder adapter Change-Id: Ic2bbb89284d095366e6a14da38099d6169d6c82f --- .../.classpath | 7 ++++ .../.project | 28 ++++++++++++++ .../.settings/org.eclipse.jdt.core.prefs | 7 ++++ .../.settings/org.eclipse.pde.core.prefs | 3 ++ .../META-INF/MANIFEST.MF | 9 +++++ .../build.properties | 4 ++ .../pom.xml | 12 ++++++ .../impl/WindFinderReportParserTest.java | 27 ++++++++++++++ .../.classpath | 7 ++++ .../.project | 28 ++++++++++++++ .../.settings/org.eclipse.jdt.core.prefs | 7 ++++ .../.settings/org.eclipse.pde.core.prefs | 3 ++ .../META-INF/MANIFEST.MF | 13 +++++++ .../build.properties | 4 ++ .../pom.xml | 12 ++++++ .../windfinderadapter/impl/Activator.java | 30 +++++++++++++++ .../impl/WindFinderReportParser.java | 37 +++++++++++++++++++ .../raceanalysis.product | 1 + java/com.sap.sailing.feature/feature.xml | 7 ++++ java/pom.xml | 2 + 20 files changed, 248 insertions(+) create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/.classpath create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/.project create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.jdt.core.prefs create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.pde.core.prefs create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/build.properties create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/pom.xml create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java create mode 100755 java/com.sap.sailing.domain.windfinderadapter/.classpath create mode 100755 java/com.sap.sailing.domain.windfinderadapter/.project create mode 100755 java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.jdt.core.prefs create mode 100755 java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.pde.core.prefs create mode 100755 java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF create mode 100755 java/com.sap.sailing.domain.windfinderadapter/build.properties create mode 100755 java/com.sap.sailing.domain.windfinderadapter/pom.xml create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/.classpath b/java/com.sap.sailing.domain.windfinderadapter.test/.classpath new file mode 100755 index 00000000000..eca7bdba8f0 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/.project b/java/com.sap.sailing.domain.windfinderadapter.test/.project new file mode 100755 index 00000000000..f4e90b744ba --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.domain.windfinderadapter.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.jdt.core.prefs new file mode 100755 index 00000000000..0c68a61dca8 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.pde.core.prefs new file mode 100755 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..34a596a3fff --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF @@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Test +Bundle-SymbolicName: com.sap.sailing.domain.windfinderadapter.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Fragment-Host: com.sap.sailing.domain.windfinderadapter +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.junit diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/build.properties b/java/com.sap.sailing.domain.windfinderadapter.test/build.properties new file mode 100755 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml b/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml new file mode 100755 index 00000000000..2357ed563fa --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.windfinderadapter.test + eclipse-test-plugin + diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java new file mode 100755 index 00000000000..6989d9bb1aa --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -0,0 +1,27 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import static org.junit.Assert.assertEquals; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.junit.Test; + +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.impl.DegreePosition; + +public class WindFinderReportParserTest { + private final String TEST_MESSAGE = "[{\"wg\":16,\"wd\":331,\"dtl\":\"2017-11-13T15:16:00+01:00\",\"dtl_s\":\"2017-11-13T15:15:30+01:00\",\"ws\":12,\"at\":7.0},{\"wg\":14,\"wd\":333,\"dtl\":\"2017-11-13T15:32:00+01:00\",\"dtl_s\":\"2017-11-13T15:31:30+01:00\",\"ws\":12,\"at\":7.0},{\"wg\":16,\"wd\":332,\"dtl\":\"2017-11-13T15:40:00+01:00\",\"dtl_s\":\"2017-11-13T15:39:30+01:00\",\"ws\":12,\"at\":7.0}]"; + + @Test + public void testReadingOneFix() throws ParseException, NumberFormatException, java.text.ParseException { + final JSONArray fullJson = (JSONArray) new JSONParser().parse(TEST_MESSAGE); + assertEquals(3, fullJson.size()); + final Wind wind = new WindFinderReportParser(new DegreePosition(0, 0)).parse((JSONObject) fullJson.get(0)); + assertEquals(12.0, wind.getKnots(), 0.0000001); + assertEquals(331.0, wind.getFrom().getDegrees(), 0.0000001); + assertEquals(0.0, wind.getPosition().getLatDeg(), 0.0000001); + assertEquals(0.0, wind.getPosition().getLngDeg(), 0.0000001); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.windfinderadapter/.classpath b/java/com.sap.sailing.domain.windfinderadapter/.classpath new file mode 100755 index 00000000000..eca7bdba8f0 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.domain.windfinderadapter/.project b/java/com.sap.sailing.domain.windfinderadapter/.project new file mode 100755 index 00000000000..8efe69d0cbe --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.domain.windfinderadapter + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.jdt.core.prefs new file mode 100755 index 00000000000..0c68a61dca8 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.pde.core.prefs new file mode 100755 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..d6653c0b03d --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Windfinderadapter +Bundle-SymbolicName: com.sap.sailing.domain.windfinderadapter +Bundle-Version: 1.0.0.qualifier +Bundle-Activator: com.sap.sailing.domain.windfinderadapter.impl.Activator +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Import-Package: org.osgi.framework;version="1.3.0" +Bundle-ActivationPolicy: lazy +Require-Bundle: org.json.simple;bundle-version="1.1.0", + com.sap.sailing.domain.common, + com.sap.sse.common diff --git a/java/com.sap.sailing.domain.windfinderadapter/build.properties b/java/com.sap.sailing.domain.windfinderadapter/build.properties new file mode 100755 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.domain.windfinderadapter/pom.xml b/java/com.sap.sailing.domain.windfinderadapter/pom.xml new file mode 100755 index 00000000000..2385819c27e --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.windfinderadapter + eclipse-plugin + diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java new file mode 100755 index 00000000000..10a559a880d --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -0,0 +1,30 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class Activator implements BundleActivator { + + private static BundleContext context; + + static BundleContext getContext() { + return context; + } + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(BundleContext bundleContext) throws Exception { + Activator.context = bundleContext; + } + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(BundleContext bundleContext) throws Exception { + Activator.context = null; + } + +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java new file mode 100755 index 00000000000..9d502656c4d --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -0,0 +1,37 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class WindFinderReportParser { + /** + * The position of the station from which messages are to be parsed + */ + private final Position position; + + /** + * Example: 2017-11-13T15:32:00+01:00 + */ + private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); + + public WindFinderReportParser(Position position) { + super(); + this.position = position; + } + + Wind parse(JSONObject jsonOfSingleMeasurement) throws NumberFormatException, ParseException { + return new WindImpl(position, new MillisecondsTimePoint(dateFormat.parse(jsonOfSingleMeasurement.get("dtl").toString())), + new KnotSpeedWithBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("ws").toString()), + new DegreeBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("wd").toString())).reverse())); + } +} diff --git a/java/com.sap.sailing.feature.p2build/raceanalysis.product b/java/com.sap.sailing.feature.p2build/raceanalysis.product index a66a3570932..34240ac81db 100755 --- a/java/com.sap.sailing.feature.p2build/raceanalysis.product +++ b/java/com.sap.sailing.feature.p2build/raceanalysis.product @@ -47,6 +47,7 @@ + diff --git a/java/com.sap.sailing.feature/feature.xml b/java/com.sap.sailing.feature/feature.xml index 4ec77eb88b3..1a126624cb7 100755 --- a/java/com.sap.sailing.feature/feature.xml +++ b/java/com.sap.sailing.feature/feature.xml @@ -67,6 +67,13 @@ Computes leaderboard information for sailing races and offers RESTful APIs to al version="0.0.0" unpack="false"/> + + com.sap.sailing.domain.expeditionadapter com.sap.sailing.domain.tractracadapter com.sap.sailing.domain.tractracadapter.persistence + com.sap.sailing.domain.windfinderadapter + com.sap.sailing.windfinderadapter.test com.sap.sailing.domain.igtimiadapter com.sap.sailing.domain.igtimiadapter.persistence com.sap.sailing.domain.racelogtrackingadapter From 4316ceb547229139d4aca619ab357b2d936cbb77 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 30 Dec 2017 11:57:40 +0100 Subject: [PATCH 009/214] fixed windfinder pom.xml file structure --- java/com.sap.sailing.domain.windfinderadapter.test/pom.xml | 2 +- java/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml b/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml index 2357ed563fa..f5a28c8a5ac 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml +++ b/java/com.sap.sailing.domain.windfinderadapter.test/pom.xml @@ -7,6 +7,6 @@ com.sap.sailing 1.0.0-SNAPSHOT - com.sap.sailing.windfinderadapter.test + com.sap.sailing.domain.windfinderadapter.test eclipse-test-plugin diff --git a/java/pom.xml b/java/pom.xml index 67c33242791..b6a3145cd62 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -109,7 +109,7 @@ com.sap.sailing.domain.tractracadapter com.sap.sailing.domain.tractracadapter.persistence com.sap.sailing.domain.windfinderadapter - com.sap.sailing.windfinderadapter.test + com.sap.sailing.domain.windfinderadapter.test com.sap.sailing.domain.igtimiadapter com.sap.sailing.domain.igtimiadapter.persistence com.sap.sailing.domain.racelogtrackingadapter From a7556bbade17980427e7c69e587713a514fb485a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 15:29:13 +0100 Subject: [PATCH 010/214] added Spot abstraction in windfinder adapter; added tests Change-Id: I679b6d2c9542641a54a8dcdaeb7bcf23ae9daa5f --- .../.classpath | 1 + .../build.properties | 3 +- .../resources/sap_schilksee_10044N.json | 1 + .../resources/schilksee_nearby.json | 17 ++++++ .../impl/WindFinderReportParserTest.java | 46 ++++++++++++++-- .../domain/windfinderadapter/Spot.java | 31 +++++++++++ .../windfinderadapter/impl/Activator.java | 38 +++++++------- .../windfinderadapter/impl/SpotImpl.java | 52 +++++++++++++++++++ .../impl/WindFinderReportParser.java | 29 ++++++++--- 9 files changed, 188 insertions(+), 30 deletions(-) create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/resources/sap_schilksee_10044N.json create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/resources/schilksee_nearby.json create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/.classpath b/java/com.sap.sailing.domain.windfinderadapter.test/.classpath index eca7bdba8f0..110168ffa19 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/.classpath +++ b/java/com.sap.sailing.domain.windfinderadapter.test/.classpath @@ -3,5 +3,6 @@ + diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/build.properties b/java/com.sap.sailing.domain.windfinderadapter.test/build.properties index 34d2e4d2dad..f11210d3f2c 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/build.properties +++ b/java/com.sap.sailing.domain.windfinderadapter.test/build.properties @@ -1,4 +1,5 @@ -source.. = src/ +source.. = src/,\ + resources/ output.. = bin/ bin.includes = META-INF/,\ . diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/resources/sap_schilksee_10044N.json b/java/com.sap.sailing.domain.windfinderadapter.test/resources/sap_schilksee_10044N.json new file mode 100755 index 00000000000..862c78b5292 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/resources/sap_schilksee_10044N.json @@ -0,0 +1 @@ +[{"wg":16,"wd":331,"dtl":"2017-11-13T15:16:00+01:00","dtl_s":"2017-11-13T15:15:30+01:00","ws":12,"at":7.0},{"wg":14,"wd":333,"dtl":"2017-11-13T15:32:00+01:00","dtl_s":"2017-11-13T15:31:30+01:00","ws":12,"at":7.0},{"wg":16,"wd":332,"dtl":"2017-11-13T15:40:00+01:00","dtl_s":"2017-11-13T15:39:30+01:00","ws":12,"at":7.0}] \ No newline at end of file diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/resources/schilksee_nearby.json b/java/com.sap.sailing.domain.windfinderadapter.test/resources/schilksee_nearby.json new file mode 100755 index 00000000000..b3f1e6c9cf2 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/resources/schilksee_nearby.json @@ -0,0 +1,17 @@ +[{ + "c": "Germany", + "lon": 10.15, + "n": "Kiel-Holtenau Airport", + "kw": "kiel-holtenau", + "lat": 54.38, + "has": "1111000", + "id": "de15" +},{ + "c": "Germany", + "lon": 10.28, + "n": "Kiel/Leuchtturm", + "kw": "kiel_leuchtturm", + "lat": 54.47, + "has": "1111010", + "id": "10044N" +}] diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java index 6989d9bb1aa..67a8d8f6084 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -2,14 +2,21 @@ package com.sap.sailing.domain.windfinderadapter.impl; import static org.junit.Assert.assertEquals; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.junit.Test; +import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.Util; public class WindFinderReportParserTest { private final String TEST_MESSAGE = "[{\"wg\":16,\"wd\":331,\"dtl\":\"2017-11-13T15:16:00+01:00\",\"dtl_s\":\"2017-11-13T15:15:30+01:00\",\"ws\":12,\"at\":7.0},{\"wg\":14,\"wd\":333,\"dtl\":\"2017-11-13T15:32:00+01:00\",\"dtl_s\":\"2017-11-13T15:31:30+01:00\",\"ws\":12,\"at\":7.0},{\"wg\":16,\"wd\":332,\"dtl\":\"2017-11-13T15:40:00+01:00\",\"dtl_s\":\"2017-11-13T15:39:30+01:00\",\"ws\":12,\"at\":7.0}]"; @@ -18,10 +25,43 @@ public class WindFinderReportParserTest { public void testReadingOneFix() throws ParseException, NumberFormatException, java.text.ParseException { final JSONArray fullJson = (JSONArray) new JSONParser().parse(TEST_MESSAGE); assertEquals(3, fullJson.size()); - final Wind wind = new WindFinderReportParser(new DegreePosition(0, 0)).parse((JSONObject) fullJson.get(0)); + final Wind wind = new WindFinderReportParser().parse(new DegreePosition(54.47, 10.28), (JSONObject) fullJson.get(0)); assertEquals(12.0, wind.getKnots(), 0.0000001); assertEquals(331.0, wind.getFrom().getDegrees(), 0.0000001); - assertEquals(0.0, wind.getPosition().getLatDeg(), 0.0000001); - assertEquals(0.0, wind.getPosition().getLngDeg(), 0.0000001); + assertPositionEquals(new DegreePosition(54.47, 10.28), wind.getPosition(), 0.000001); + } + + @Test + public void testReadingSeveralFixesFromStream() throws ParseException, NumberFormatException, java.text.ParseException, IOException { + final Reader reader = new InputStreamReader(getClass().getResourceAsStream("/sap_schilksee_10044N.json")); + final JSONArray fullJson = (JSONArray) new JSONParser().parse(reader); + assertEquals(3, fullJson.size()); + final Wind wind = new WindFinderReportParser().parse(new DegreePosition(54.47, 10.28), (JSONObject) fullJson.get(0)); + assertEquals(12.0, wind.getKnots(), 0.0000001); + assertEquals(331.0, wind.getFrom().getDegrees(), 0.0000001); + assertPositionEquals(new DegreePosition(54.47, 10.28), wind.getPosition(), 0.000001); + } + + @Test + public void testReadingSpotDescriptions() throws IOException, ParseException { + final Reader reader = new InputStreamReader(getClass().getResourceAsStream("/schilksee_nearby.json")); + final JSONArray fullJson = (JSONArray) new JSONParser().parse(reader); + assertEquals(2, fullJson.size()); + final Iterable spots = new WindFinderReportParser().parseSpots(fullJson); + final Spot kielHoltenau = (Spot) Util.get(spots, 0); + final Spot kielLeuchtturm = (Spot) Util.get(spots, 1); + assertEquals("Kiel-Holtenau Airport", kielHoltenau.getName()); + assertEquals("kiel-holtenau", kielHoltenau.getKeyword()); + assertEquals("de15", kielHoltenau.getId()); + assertPositionEquals(new DegreePosition(54.38, 10.15), kielHoltenau.getPosition(), 0.000001); + assertEquals("Kiel/Leuchtturm", kielLeuchtturm.getName()); + assertEquals("kiel_leuchtturm", kielLeuchtturm.getKeyword()); + assertEquals("10044N", kielLeuchtturm.getId()); + assertPositionEquals(new DegreePosition(54.47, 10.28), kielLeuchtturm.getPosition(), 0.000001); + } + + private static void assertPositionEquals(Position p1, Position p2, double degreeDelta) { + assertEquals(p1.getLatDeg(), p2.getLatDeg(), degreeDelta); + assertEquals(p1.getLngDeg(), p2.getLngDeg(), degreeDelta); } } \ No newline at end of file diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java new file mode 100755 index 00000000000..bcf814921db --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -0,0 +1,31 @@ +package com.sap.sailing.domain.windfinderadapter; + +import java.net.MalformedURLException; +import java.net.URL; + +import com.sap.sailing.domain.common.Positioned; +import com.sap.sse.common.NamedWithID; + +/** + * A measurement spot for which a report and / or a forecast may exist. + * + * @author Axel Uhl (D043530) + * + */ +public interface Spot extends NamedWithID, Positioned { + /** + * A spot's ID is always a {@link String} + */ + @Override + String getId(); + + /** + * A keyword that is used, in particular, when constructing the URLs for {@link #getReportUrl() report} and + * {@link #getForecastUrl() forecast} web pages. + */ + String getKeyword(); + + URL getReportUrl() throws MalformedURLException; + + URL getForecastUrl() throws MalformedURLException; +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java index 10a559a880d..afbba66e888 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -5,26 +5,28 @@ import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { - private static BundleContext context; + private static BundleContext context; - static BundleContext getContext() { - return context; - } + static BundleContext getContext() { + return context; + } - /* - * (non-Javadoc) - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(BundleContext bundleContext) throws Exception { - Activator.context = bundleContext; - } + /* + * (non-Javadoc) + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(BundleContext bundleContext) throws Exception { + Activator.context = bundleContext; + } - /* - * (non-Javadoc) - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(BundleContext bundleContext) throws Exception { - Activator.context = null; - } + /* + * (non-Javadoc) + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(BundleContext bundleContext) throws Exception { + Activator.context = null; + } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java new file mode 100755 index 00000000000..0fae0623e63 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -0,0 +1,52 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import java.net.MalformedURLException; +import java.net.URL; + +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.impl.NamedImpl; + +public class SpotImpl extends NamedImpl implements Spot { + private static final long serialVersionUID = 1642900468710612984L; + private static final String BASE_URL = "https://www.windfinder.com"; + private static final String BASE_REPORT_URL = BASE_URL + "/report"; + private static final String BASE_FORECAST_URL = BASE_URL + "/forecast"; + + private final String id; + private final String keyword; + private final Position position; + + public SpotImpl(String name, String id, String keyword, Position position) { + super(name); + this.id = id; + this.keyword = keyword; + this.position = position; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getKeyword() { + return keyword; + } + + @Override + public URL getReportUrl() throws MalformedURLException { + return new URL(BASE_REPORT_URL+"/"+getKeyword()); + } + + @Override + public URL getForecastUrl() throws MalformedURLException { + return new URL(BASE_FORECAST_URL+"/"+getKeyword()); + } + +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index 9d502656c4d..f94be9a138e 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -4,34 +4,47 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import org.json.simple.JSONArray; import org.json.simple.JSONObject; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; public class WindFinderReportParser { - /** - * The position of the station from which messages are to be parsed - */ - private final Position position; - /** * Example: 2017-11-13T15:32:00+01:00 */ private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); - public WindFinderReportParser(Position position) { + public WindFinderReportParser() { super(); - this.position = position; } - Wind parse(JSONObject jsonOfSingleMeasurement) throws NumberFormatException, ParseException { + /** + * @param position The position of the station from which messages are to be parsed + */ + Wind parse(Position position, JSONObject jsonOfSingleMeasurement) throws NumberFormatException, ParseException { return new WindImpl(position, new MillisecondsTimePoint(dateFormat.parse(jsonOfSingleMeasurement.get("dtl").toString())), new KnotSpeedWithBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("ws").toString()), new DegreeBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("wd").toString())).reverse())); + } + + Spot parseSpot(JSONObject jsonOfSingleSpot) { + return new SpotImpl(jsonOfSingleSpot.get("n").toString(), + jsonOfSingleSpot.get("id").toString(), + jsonOfSingleSpot.get("kw").toString(), + new DegreePosition(((Number) jsonOfSingleSpot.get("lat")).doubleValue(), + ((Number) jsonOfSingleSpot.get("lon")).doubleValue())); + } + + Iterable parseSpots(JSONArray jsonOfMultipleSpots) { + return Util.map(jsonOfMultipleSpots, jsonOfSingleSpot->parseSpot((JSONObject) jsonOfSingleSpot)); } } From 2ef71fdf9c7ecab9130e8a547d60ce1e42519818 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 15:31:25 +0100 Subject: [PATCH 011/214] added URL tests for Spot interface Change-Id: I0469cebb02fa5cd63338e1e0e99fece2937d03b1 --- .../windfinderadapter/impl/WindFinderReportParserTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java index 67a8d8f6084..c4afd2bad4d 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -53,10 +53,14 @@ public class WindFinderReportParserTest { assertEquals("Kiel-Holtenau Airport", kielHoltenau.getName()); assertEquals("kiel-holtenau", kielHoltenau.getKeyword()); assertEquals("de15", kielHoltenau.getId()); + assertEquals("https://www.windfinder.com/report/kiel-holtenau", kielHoltenau.getReportUrl().toString()); + assertEquals("https://www.windfinder.com/forecast/kiel-holtenau", kielHoltenau.getForecastUrl().toString()); assertPositionEquals(new DegreePosition(54.38, 10.15), kielHoltenau.getPosition(), 0.000001); assertEquals("Kiel/Leuchtturm", kielLeuchtturm.getName()); assertEquals("kiel_leuchtturm", kielLeuchtturm.getKeyword()); assertEquals("10044N", kielLeuchtturm.getId()); + assertEquals("https://www.windfinder.com/report/kiel_leuchtturm", kielLeuchtturm.getReportUrl().toString()); + assertEquals("https://www.windfinder.com/forecast/kiel_leuchtturm", kielLeuchtturm.getForecastUrl().toString()); assertPositionEquals(new DegreePosition(54.47, 10.28), kielLeuchtturm.getPosition(), 0.000001); } From 6e88b42ffbb97efeb75869330d77ed1bf8ac5a10 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 17:08:51 +0100 Subject: [PATCH 012/214] added Javadoc and the Statistics URL Change-Id: I36f2e9d375ed455f0d1d13d3091dc6f51b0b6637 --- .../sap/sailing/domain/windfinderadapter/Spot.java | 11 +++++++++++ .../domain/windfinderadapter/impl/SpotImpl.java | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java index bcf814921db..c1984c0f141 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -25,7 +25,18 @@ public interface Spot extends NamedWithID, Positioned { */ String getKeyword(); + /** + * @return the URL of a web page showing a current weather report for this spot + */ URL getReportUrl() throws MalformedURLException; + /** + * @return the URL of a web page showing a weather forecast for this spot + */ URL getForecastUrl() throws MalformedURLException; + + /** + * @return the URL of a web page showing general wind statistics for this spot + */ + URL getStatisticsUrl() throws MalformedURLException; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index 0fae0623e63..106ed8ba5f5 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -12,6 +12,7 @@ public class SpotImpl extends NamedImpl implements Spot { private static final String BASE_URL = "https://www.windfinder.com"; private static final String BASE_REPORT_URL = BASE_URL + "/report"; private static final String BASE_FORECAST_URL = BASE_URL + "/forecast"; + private static final String BASE_STATISTICS_URL = BASE_URL + "/windstatistics"; private final String id; private final String keyword; @@ -49,4 +50,8 @@ public class SpotImpl extends NamedImpl implements Spot { return new URL(BASE_FORECAST_URL+"/"+getKeyword()); } + @Override + public URL getStatisticsUrl() throws MalformedURLException { + return new URL(BASE_STATISTICS_URL+"/"+getKeyword()); + } } From a67e921d1cd6e3fbfd9cb90118369f935fdeb50e Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 18:07:11 +0100 Subject: [PATCH 013/214] added a bit of a frame for a WindFinder connector Change-Id: Idfbc72973da480f70e631c80b068671aa341060d --- .../sailing/domain/common/WindSourceType.java | 9 +- .../META-INF/MANIFEST.MF | 4 +- .../domain/windfinderadapter/Spot.java | 5 ++ .../windfinderadapter/impl/Activator.java | 21 +++++ .../windfinderadapter/impl/SpotImpl.java | 30 ++++++- .../impl/WindFinderReportParser.java | 17 +++- .../impl/WindFinderTrackerFactory.java | 49 +++++++++++ .../impl/WindFinderWindTracker.java | 82 +++++++++++++++++++ 8 files changed, 212 insertions(+), 5 deletions(-) create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindSourceType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindSourceType.java index b4246a1c5d3..594763771de 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindSourceType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindSourceType.java @@ -51,7 +51,14 @@ public enum WindSourceType { * Like {@link #COMBINED}, only that when combined with an ID into a {@link WindSourceWithAdditionalID} then the * position for the wind is taken to be the middle of the tracked leg whose number is identified by the ID. */ - LEG_MIDDLE(/* can be stored */ false, /* base confidence */ 0.9, /* useSpeed */ true); + LEG_MIDDLE(/* can be stored */ false, /* base confidence */ 0.9, /* useSpeed */ true), + + /** + * A wind measurement spot as provided by https://www.windfinder.com; when used with a + * {@link WindSourceWithAdditionalID}, the {@link WindSourceWithAdditionalID#getId() ID} is + * the WindFinder's "Spot ID" such as "10044N" for Kiel Lighthouse. + */ + WINDFINDER(/* can be stored */ true, /* base confidence */ 0.3, /* useSpeed */ true); private final boolean canBeStored; diff --git a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF index d6653c0b03d..a075b588a39 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF @@ -10,4 +10,6 @@ Import-Package: org.osgi.framework;version="1.3.0" Bundle-ActivationPolicy: lazy Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sap.sailing.domain.common, - com.sap.sse.common + com.sap.sse.common, + com.sap.sailing.domain, + com.sap.sse diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java index c1984c0f141..b61f65f8ffa 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -1,9 +1,12 @@ package com.sap.sailing.domain.windfinderadapter; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.text.ParseException; import com.sap.sailing.domain.common.Positioned; +import com.sap.sailing.domain.common.Wind; import com.sap.sse.common.NamedWithID; /** @@ -39,4 +42,6 @@ public interface Spot extends NamedWithID, Positioned { * @return the URL of a web page showing general wind statistics for this spot */ URL getStatisticsUrl() throws MalformedURLException; + + Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java index afbba66e888..40225e629f0 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -1,11 +1,24 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +import com.sap.sailing.domain.tracking.WindTrackerFactory; public class Activator implements BundleActivator { + private static final Logger logger = Logger.getLogger(Activator.class.getName()); private static BundleContext context; + + /** + * Registrations of OSGi services to be de-registered when the bundle shuts down + */ + private Set> registrations = new HashSet<>(); static BundleContext getContext() { return context; @@ -18,6 +31,10 @@ public class Activator implements BundleActivator { */ public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; + logger.info("Creating ExpeditionTrackerFactory"); + final WindFinderTrackerFactory windfinderTrackerFactory = new WindFinderTrackerFactory(); + registrations.add(context.registerService(WindFinderTrackerFactory.class, windfinderTrackerFactory, /* properties */null)); + registrations.add(context.registerService(WindTrackerFactory.class, windfinderTrackerFactory, /* properties */null)); } /* @@ -26,6 +43,10 @@ public class Activator implements BundleActivator { * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext bundleContext) throws Exception { + for (ServiceRegistration reg : registrations) { + reg.unregister(); + } + registrations.clear(); Activator.context = null; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index 106ed8ba5f5..30d74ff1f4d 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -1,10 +1,17 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.text.ParseException; + +import org.json.simple.JSONArray; +import org.json.simple.parser.JSONParser; import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.Util; import com.sap.sse.common.impl.NamedImpl; public class SpotImpl extends NamedImpl implements Spot { @@ -17,12 +24,14 @@ public class SpotImpl extends NamedImpl implements Spot { private final String id; private final String keyword; private final Position position; + private final WindFinderReportParser parser; - public SpotImpl(String name, String id, String keyword, Position position) { + public SpotImpl(String name, String id, String keyword, Position position, WindFinderReportParser parser) { super(name); this.id = id; this.keyword = keyword; this.position = position; + this.parser = parser; } @Override @@ -54,4 +63,23 @@ public class SpotImpl extends NamedImpl implements Spot { public URL getStatisticsUrl() throws MalformedURLException { return new URL(BASE_STATISTICS_URL+"/"+getKeyword()); } + + @Override + public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException { + final String response = (String) getMeasurementsUrl().getContent(); + final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(response)); + final Wind result; + if (measurements != null && !Util.isEmpty(measurements)) { + result = Util.last(measurements); + } else { + result = null; + } + return result; + } + + private URL getMeasurementsUrl() throws MalformedURLException { + // TODO this hasn't been aligned with WindFinder yet; so far we're seeing "random" filenames in the URLs such as sap_schilksee_10044N.json... +// return new URL("http://external.windfinder.com/sap_"+getId()+".json"); + return new URL("http://external.windfinder.com/sap_schilksee_10044N.json"); + } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index f94be9a138e..3e36dda8f79 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -3,6 +3,8 @@ package com.sap.sailing.domain.windfinderadapter.impl; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -34,14 +36,25 @@ public class WindFinderReportParser { return new WindImpl(position, new MillisecondsTimePoint(dateFormat.parse(jsonOfSingleMeasurement.get("dtl").toString())), new KnotSpeedWithBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("ws").toString()), new DegreeBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("wd").toString())).reverse())); - } + } + + /** + * @param position The position of the station from which messages are to be parsed + */ + Iterable parse(Position position, JSONArray jsonOfSeveralMeasurements) throws NumberFormatException, ParseException { + final List result = new ArrayList<>(); + for (final Object jsonOfSingleMeasurement : jsonOfSeveralMeasurements) { + result.add(parse(position, (JSONObject) jsonOfSingleMeasurement)); + } + return result; + } Spot parseSpot(JSONObject jsonOfSingleSpot) { return new SpotImpl(jsonOfSingleSpot.get("n").toString(), jsonOfSingleSpot.get("id").toString(), jsonOfSingleSpot.get("kw").toString(), new DegreePosition(((Number) jsonOfSingleSpot.get("lat")).doubleValue(), - ((Number) jsonOfSingleSpot.get("lon")).doubleValue())); + ((Number) jsonOfSingleSpot.get("lon")).doubleValue()), this); } Iterable parseSpots(JSONArray jsonOfMultipleSpots) { diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java new file mode 100755 index 00000000000..c9c45f843b1 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java @@ -0,0 +1,49 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.WindTracker; +import com.sap.sailing.domain.tracking.WindTrackerFactory; + +public class WindFinderTrackerFactory implements WindTrackerFactory { + private final Map windTrackerPerRace; + + public WindFinderTrackerFactory() { + windTrackerPerRace = new HashMap<>(); + } + + @Override + public WindTracker createWindTracker(DynamicTrackedRegatta trackedRegatta, RaceDefinition race, + boolean correctByDeclination) throws Exception { + final WindTracker result; + synchronized (windTrackerPerRace) { + final WindTracker existingWindTrackerForRace = getExistingWindTracker(race); + if (existingWindTrackerForRace == null) { + final DynamicTrackedRace trackedRace = trackedRegatta.getTrackedRace(race); + result = new WindFinderWindTracker(trackedRace, correctByDeclination, this); + windTrackerPerRace.put(race, result); + } else { + result = existingWindTrackerForRace; + } + } + return result; + } + + @Override + public WindTracker getExistingWindTracker(RaceDefinition race) { + synchronized (windTrackerPerRace) { + return windTrackerPerRace.get(race); + } + } + + void trackerStopped(RaceDefinition race) { + synchronized (windTrackerPerRace) { + windTrackerPerRace.remove(race); + } + } + +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java new file mode 100755 index 00000000000..5581a926c1c --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -0,0 +1,82 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.WindTracker; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.Duration; +import com.sap.sse.util.ThreadPoolUtil; + +/** + * When an object of this tracker type is created and there are WindFinder stations available, it schedules regular + * requests to those WindFinder spots that are deemed useful for the race. If no location information for the race and + * fleet is available yet, all sources available are considered useful. Otherwise, a rough distance check is performed. + * If a measurement spot is ridiculously far away from the race course it doesn't make sense to even poll its + * measurements, even if the general weighted average would ultimately rank down its contribution. + *

+ * + * {@link #stop() Stopping} this tracker will cancel the task that polls for regular updates and will tell the + * {@link WindFinderTrackerFactory} that this tracker has been stopped. The tracker will therefore be released by the + * factory and will no longer be returned as the tracker responsible for retrieving WindFinder data for the particular + * race that this tracker was bound to. + * + * @author Axel Uhl (D043530) + * + */ +public class WindFinderWindTracker implements WindTracker, Runnable { + private static final Logger logger = Logger.getLogger(WindFinderWindTracker.class.getName()); + private static final Duration POLL_EVERY = Duration.ONE_MINUTE; + + private final DynamicTrackedRace trackedRace; + private final boolean correctByDeclination; + private final WindFinderTrackerFactory factory; + private final WindFinderReportParser parser; + + private ScheduledFuture poller; + + public WindFinderWindTracker(DynamicTrackedRace trackedRace, boolean correctByDeclination, WindFinderTrackerFactory factory) { + this.trackedRace = trackedRace; + this.correctByDeclination = correctByDeclination; + this.factory = factory; + this.parser = new WindFinderReportParser(); + this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this, + /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS); + } + + /** + * This method is executed in order to poll the WindFinder spots for new measurements + */ + @Override + public void run() { + try { + final Iterable usefulSpots = getUsefulSpots(); + for (final Spot usefulSpot : usefulSpots) { + final Wind wind = usefulSpot.getLatestMeasurement(); + if (wind != null) { + trackedRace.recordWind(wind, new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId())); + } + } + } catch (Exception e) { + logger.log(Level.WARNING, "Exception trying to obtain WindFinder data for race "+trackedRace.getRace().getName(), e); + } + } + + private Iterable getUsefulSpots() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void stop() { + poller.cancel(/* mayInterruptIfRunning */ false); + factory.trackerStopped(trackedRace.getRace()); + } + +} From 26bd80618e18e86629256928cab3067ac6b5ffea Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 18:41:39 +0100 Subject: [PATCH 014/214] a first tiny test for using the WindTracker Change-Id: I0b46ef9c8fb235ce8e6fb4fd856800722d193aa9 --- .../META-INF/MANIFEST.MF | 4 +- .../impl/WindFinderPollerTest.java | 54 +++++++++++++++++++ .../windfinderadapter/impl/SpotImpl.java | 6 ++- .../impl/WindFinderWindTracker.java | 14 ++++- 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100755 java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF index 34a596a3fff..4107bed7533 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF @@ -6,4 +6,6 @@ Bundle-Version: 1.0.0.qualifier Bundle-Vendor: SAP Fragment-Host: com.sap.sailing.domain.windfinderadapter Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Require-Bundle: org.junit +Require-Bundle: org.junit, + org.mockito.mockito-core;bundle-version="1.10.14", + org.objenesis;bundle-version="2.1.0" diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java new file mode 100755 index 00000000000..81ce401391a --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java @@ -0,0 +1,54 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.WindTracker; +import com.sap.sse.common.Util.Pair; + +public class WindFinderPollerTest { + @Test + public void testPolling() throws Exception { + WindFinderTrackerFactory factory = new WindFinderTrackerFactory(); + final List> wind = new ArrayList<>(); // TODO use blocking queue and read from it with a timeout + final RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class); + final DynamicTrackedRegatta mockedTrackedRegatta = mock(DynamicTrackedRegatta.class); + final DynamicTrackedRace mockedTrackedRace = mock(DynamicTrackedRace.class); + when(mockedTrackedRegatta.getExistingTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace); + when(mockedTrackedRegatta.getTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace); + when(mockedTrackedRace.recordWind(any(Wind.class), any(WindSourceWithAdditionalID.class))).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + wind.add(new Pair<>(invocation.getArgumentAt(0, Wind.class), invocation.getArgumentAt(1, WindSourceWithAdditionalID.class))); + return true; + } + }); + when(mockedTrackedRace.recordWind(any(Wind.class), any(WindSourceWithAdditionalID.class), /* applyFilter */ anyBoolean())).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + wind.add(new Pair<>(invocation.getArgumentAt(0, Wind.class), invocation.getArgumentAt(1, WindSourceWithAdditionalID.class))); + return true; + } + }); + WindTracker tracker = factory.createWindTracker(mockedTrackedRegatta, mockedRaceDefinition, /* correctByDeclination */ false); + Thread.sleep(5000); // TODO see above; use blocking queue and timeout + tracker.stop(); + assertEquals(1, wind.size()); // one latest measurement + } +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index 30d74ff1f4d..cc0105cb761 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -1,6 +1,8 @@ package com.sap.sailing.domain.windfinderadapter.impl; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; @@ -66,8 +68,8 @@ public class SpotImpl extends NamedImpl implements Spot { @Override public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException { - final String response = (String) getMeasurementsUrl().getContent(); - final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(response)); + final InputStream response = (InputStream) getMeasurementsUrl().getContent(); + final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(new InputStreamReader(response))); final Wind result; if (measurements != null && !Util.isEmpty(measurements)) { result = Util.last(measurements); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index 5581a926c1c..f2cf5e5cfff 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -1,5 +1,6 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.util.Collections; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -7,6 +8,7 @@ import java.util.logging.Logger; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.WindTracker; @@ -70,7 +72,17 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private Iterable getUsefulSpots() { // TODO Auto-generated method stub - return null; + // mocking with the single spot we also have mocked the spot measurement URL for: +// { +// "c": "Germany", +// "lon": 10.28, +// "n": "Kiel/Leuchtturm", +// "kw": "kiel_leuchtturm", +// "lat": 54.47, +// "has": "1111010", +// "id": "10044N" +// } + return Collections.singleton(new SpotImpl("Kiel/Leuchtturm", "10044N", "kiel_leuchtturm", new DegreePosition(54.47, 10.28), parser)); } @Override From f08fc5432ea63181ea72b357b1d3c0aefbb98dbb Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 2 Jan 2018 23:11:54 +0100 Subject: [PATCH 015/214] added hamcrest to windfinder.test fragment dependencies Change-Id: I4ce075256d01b86b90ec5a0df6090f44578bc4b4 --- .../META-INF/MANIFEST.MF | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF index 4107bed7533..bcb83ed79e7 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter.test/META-INF/MANIFEST.MF @@ -8,4 +8,5 @@ Fragment-Host: com.sap.sailing.domain.windfinderadapter Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Require-Bundle: org.junit, org.mockito.mockito-core;bundle-version="1.10.14", - org.objenesis;bundle-version="2.1.0" + org.objenesis;bundle-version="2.1.0", + org.hamcrest;bundle-version="1.1.0" From 944942cdda239477eacecadf04775904959ff73a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 3 Jan 2018 11:35:12 +0100 Subject: [PATCH 016/214] added wind source strings for WINDFINDER source type Change-Id: I17620dc99f5dd51f843db48b2f5ef4ab76759185 --- .../java/com/sap/sailing/gwt/ui/client/StringMessages.java | 2 ++ .../com/sap/sailing/gwt/ui/client/StringMessages.properties | 2 ++ .../sap/sailing/gwt/ui/client/StringMessages_de.properties | 2 ++ .../sap/sailing/gwt/ui/client/WindSourceTypeFormatter.java | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 5e6e389dcea..c7d76702f16 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2026,4 +2026,6 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String importFinishedGotoRaceboard(); String importFinishedGotoEvent(); String chooseAName(); + String windFinderWindSourceTypeName(); + String windFinderWindSourceTypeTooltip(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index da37a366a4a..4b696c8b0b3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2015,3 +2015,5 @@ importFinishedMessage=Successfully imported expedition data. importFinishedGotoRaceboard=Open raceboard importFinishedGotoEvent=Open event chooseAName=Choose a name +windFinderWindSourceTypeName=WindFinder +windFinderWindSourceTypeTooltip=Measured wind from one or more spots of www.windfinder.com \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index f4e7f22ec96..ea8a7ab0181 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2009,3 +2009,5 @@ importFinishedMessage=Erfolgreich Expeditionsdaten eingelesen importFinishedGotoRaceboard=Raceboard öffnen importFinishedGotoEvent=Event öffnen chooseAName=Namen auswählen +windFinderWindSourceTypeName=WindFinder +windFinderWindSourceTypeTooltip=Gemessener Wind von einer oder mehreren Stationen von www.windfinder.com \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/WindSourceTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/WindSourceTypeFormatter.java index dc8364c51b8..4a19e806a3d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/WindSourceTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/WindSourceTypeFormatter.java @@ -20,6 +20,8 @@ public class WindSourceTypeFormatter { return stringMessages.webWindSourceTypeName(); case RACECOMMITTEE: return stringMessages.raceCommitteeWindSourceTypeName(); + case WINDFINDER: + return stringMessages.windFinderWindSourceTypeName(); } return null; } @@ -40,6 +42,8 @@ public class WindSourceTypeFormatter { return stringMessages.webWindSourceTypeTooltip(); case RACECOMMITTEE: return stringMessages.raceCommitteeWindSourceTypeTooltip(); + case WINDFINDER: + return stringMessages.windFinderWindSourceTypeTooltip(); } return null; } From 23b44b9a66e1dc902a27c48b6d65c63f3deea92d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 3 Jan 2018 17:22:54 +0100 Subject: [PATCH 017/214] added script for summing up the unique visitors per year Change-Id: I54f2c6c825b2ab8581ab4cd80e57206c19dea5a1 --- configuration/uniqueIpsPerYear | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 configuration/uniqueIpsPerYear diff --git a/configuration/uniqueIpsPerYear b/configuration/uniqueIpsPerYear new file mode 100755 index 00000000000..de17e124c8b --- /dev/null +++ b/configuration/uniqueIpsPerYear @@ -0,0 +1,10 @@ +#!/bin/sh +year=$1 +read -s -p "Password for user awstats on awstats.sapsailing.com: " passwd +if [ "$year" = "" ]; then + year=`date +%Y` +fi +for i in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec; do + echo -n "$i: " + wget -O - "http://awstats:$passwd@awstats.sapsailing.com/unique-visitors/unique-ips-days-useragents-per-event-$i-$year" 2>/dev/null | awk '{ sum=sum+$2; } END { print sum; }' +done | awk '{ print $0; total=total+$2; } END { print "Total: " total; }' From bd30b8b100b790ec02d84ea57ac10b989b613d48 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 3 Jan 2018 18:15:18 +0100 Subject: [PATCH 018/214] added test for wind source ID Change-Id: I2681f36ec51ce4087f00d63509015224fe332ab3 --- .../impl/WindFinderPollerTest.java | 23 +++++-- .../impl/WindFinderReportParserTest.java | 20 +++++- .../ReviewedSpotsCollection.java | 26 +++++++ .../domain/windfinderadapter/Spot.java | 8 ++- .../windfinderadapter/impl/Activator.java | 2 + .../impl/ReviewedSpotsCollectionImpl.java | 69 +++++++++++++++++++ .../windfinderadapter/impl/SpotImpl.java | 49 +++++++++++-- .../impl/WindFinderReportParser.java | 10 +-- .../impl/WindFinderTrackerFactory.java | 28 +++++++- .../impl/WindFinderWindTracker.java | 35 ++++------ 10 files changed, 232 insertions(+), 38 deletions(-) create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java index 81ce401391a..c5c77b06628 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java @@ -1,12 +1,15 @@ package com.sap.sailing.domain.windfinderadapter.impl; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import org.junit.Test; @@ -25,8 +28,10 @@ import com.sap.sse.common.Util.Pair; public class WindFinderPollerTest { @Test public void testPolling() throws Exception { + final Object monitor = new Object(); WindFinderTrackerFactory factory = new WindFinderTrackerFactory(); - final List> wind = new ArrayList<>(); // TODO use blocking queue and read from it with a timeout + factory.addReviewedSpotCollection(new ReviewedSpotsCollectionImpl("schilksee")); + final List> wind = new ArrayList<>(); final RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class); final DynamicTrackedRegatta mockedTrackedRegatta = mock(DynamicTrackedRegatta.class); final DynamicTrackedRace mockedTrackedRace = mock(DynamicTrackedRace.class); @@ -36,6 +41,9 @@ public class WindFinderPollerTest { @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { wind.add(new Pair<>(invocation.getArgumentAt(0, Wind.class), invocation.getArgumentAt(1, WindSourceWithAdditionalID.class))); + synchronized (monitor) { + monitor.notifyAll(); + } return true; } }); @@ -46,9 +54,14 @@ public class WindFinderPollerTest { return true; } }); - WindTracker tracker = factory.createWindTracker(mockedTrackedRegatta, mockedRaceDefinition, /* correctByDeclination */ false); - Thread.sleep(5000); // TODO see above; use blocking queue and timeout + final WindTracker tracker; + synchronized (monitor) { + tracker = factory.createWindTracker(mockedTrackedRegatta, mockedRaceDefinition, /* correctByDeclination */ false); + monitor.wait(5000); + } tracker.stop(); - assertEquals(1, wind.size()); // one latest measurement + assertFalse(wind.isEmpty()); // at least one latest measurement + Pair firstFix = wind.iterator().next(); + assertTrue(new HashSet<>(Arrays.asList("de15", "10044N")).contains(((WindSourceWithAdditionalID) firstFix.getB()).getId())); } } diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java index c4afd2bad4d..0a92cd18581 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -5,6 +5,10 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -47,7 +51,8 @@ public class WindFinderReportParserTest { final Reader reader = new InputStreamReader(getClass().getResourceAsStream("/schilksee_nearby.json")); final JSONArray fullJson = (JSONArray) new JSONParser().parse(reader); assertEquals(2, fullJson.size()); - final Iterable spots = new WindFinderReportParser().parseSpots(fullJson); + final WindFinderReportParser parser = new WindFinderReportParser(); + final Iterable spots = parser.parseSpots(fullJson, new ReviewedSpotsCollectionImpl("schilksee")); final Spot kielHoltenau = (Spot) Util.get(spots, 0); final Spot kielLeuchtturm = (Spot) Util.get(spots, 1); assertEquals("Kiel-Holtenau Airport", kielHoltenau.getName()); @@ -63,6 +68,19 @@ public class WindFinderReportParserTest { assertEquals("https://www.windfinder.com/forecast/kiel_leuchtturm", kielLeuchtturm.getForecastUrl().toString()); assertPositionEquals(new DegreePosition(54.47, 10.28), kielLeuchtturm.getPosition(), 0.000001); } + + @Test + public void testTwoSpotsInSchilksee() throws MalformedURLException, IOException, ParseException { + assertEquals(2, Util.size(new ReviewedSpotsCollectionImpl("schilksee").getSpots())); + } + + @Test + public void testSpotsInSchilkseeAreThoseWithIds_10044N_And_de15() throws MalformedURLException, IOException, ParseException { + final Iterable spots = new ReviewedSpotsCollectionImpl("schilksee").getSpots(); + final Set spotIds = new HashSet<>(); + Util.addAll(Util.map(spots, s->s.getId()), spotIds); + assertEquals(new HashSet<>(Arrays.asList("10044N", "de15")), spotIds); + } private static void assertPositionEquals(Position p1, Position p2, double degreeDelta) { assertEquals(p1.getLatDeg(), p2.getLatDeg(), degreeDelta); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java new file mode 100755 index 00000000000..9bb0667317d --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java @@ -0,0 +1,26 @@ +package com.sap.sailing.domain.windfinderadapter; + +import java.io.IOException; +import java.net.MalformedURLException; + +import org.json.simple.parser.ParseException; + +import com.sap.sse.common.WithID; + +/** + * A set of {@link Spot}s that WindFinder has reviewed and selected for a dedicated request for + * a location, such as a sailing club or an event venue. The collection has a {@link String} as + * a dedicated ID which is used in URL construction to obtain the {@link Spot}s in the collection + * as well as the reports from those spots.

+ * + * Equality and hash code are based only on the {@link #getId() ID}. + * + * @author Axel Uhl (D043530) + * + */ +public interface ReviewedSpotsCollection extends WithID { + @Override + String getId(); + + Iterable getSpots() throws MalformedURLException, IOException, ParseException; +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java index b61f65f8ffa..f3601e27294 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -10,7 +10,8 @@ import com.sap.sailing.domain.common.Wind; import com.sap.sse.common.NamedWithID; /** - * A measurement spot for which a report and / or a forecast may exist. + * A measurement spot for which a report and / or a forecast may exist. Equality and + * hash code are based on the {@link #getId() ID} only. * * @author Axel Uhl (D043530) * @@ -28,6 +29,11 @@ public interface Spot extends NamedWithID, Positioned { */ String getKeyword(); + /** + * Name of the country this spot is in, provided in the English language + */ + String getEnglishCountryName(); + /** * @return the URL of a web page showing a current weather report for this spot */ diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java index 40225e629f0..03235f1d0bf 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -12,6 +12,8 @@ import com.sap.sailing.domain.tracking.WindTrackerFactory; public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); + + static final String BASE_URL_FOR_JSON_DOCUMENTS = "http://external.windfinder.com/sap"; private static BundleContext context; diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java new file mode 100755 index 00000000000..4e7ca9fe25c --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java @@ -0,0 +1,69 @@ +package com.sap.sailing.domain.windfinderadapter.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; + +import org.json.simple.JSONArray; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinderadapter.Spot; + +public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { + /** + * What needs to be appended to the spot collection {@link #id} in order + * to obtain the file name of the document that has the JSON array with + * the spot descriptions. + */ + private final static String SPOT_LIST_DOCUMENT_SUFFIX = "_nearby.json"; + + private final String id; + private final WindFinderReportParser parser; + + public ReviewedSpotsCollectionImpl(String id) { + this.id = id; + this.parser = new WindFinderReportParser(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ReviewedSpotsCollectionImpl other = (ReviewedSpotsCollectionImpl) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String getId() { + return id; + } + + @Override + public Iterable getSpots() throws MalformedURLException, IOException, ParseException { + JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(new InputStreamReader( + (InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent())); + return parser.parseSpots(spotsAsJson, this); + } + +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index cc0105cb761..35493ef96cd 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -6,12 +6,16 @@ import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.json.simple.JSONArray; import org.json.simple.parser.JSONParser; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; import com.sap.sailing.domain.windfinderadapter.Spot; import com.sap.sse.common.Util; import com.sap.sse.common.impl.NamedImpl; @@ -25,17 +29,46 @@ public class SpotImpl extends NamedImpl implements Spot { private final String id; private final String keyword; + private final String englishCountryName; private final Position position; private final WindFinderReportParser parser; + private final ReviewedSpotsCollection collection; - public SpotImpl(String name, String id, String keyword, Position position, WindFinderReportParser parser) { + public SpotImpl(String name, String id, String keyword, String englishCountryName, Position position, WindFinderReportParser parser, ReviewedSpotsCollection collection) { super(name); this.id = id; this.keyword = keyword; + this.englishCountryName = englishCountryName; this.position = position; this.parser = parser; + this.collection = collection; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SpotImpl other = (SpotImpl) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + @Override public Position getPosition() { return position; @@ -51,6 +84,11 @@ public class SpotImpl extends NamedImpl implements Spot { return keyword; } + @Override + public String getEnglishCountryName() { + return englishCountryName; + } + @Override public URL getReportUrl() throws MalformedURLException { return new URL(BASE_REPORT_URL+"/"+getKeyword()); @@ -72,7 +110,10 @@ public class SpotImpl extends NamedImpl implements Spot { final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(new InputStreamReader(response))); final Wind result; if (measurements != null && !Util.isEmpty(measurements)) { - result = Util.last(measurements); + final List measurementsSortedByTimepoint = new ArrayList<>(); + Util.addAll(measurements, measurementsSortedByTimepoint); + Collections.sort(measurementsSortedByTimepoint, (w1, w2)->w1.getTimePoint().compareTo(w2.getTimePoint())); + result = Util.last(measurementsSortedByTimepoint); } else { result = null; } @@ -80,8 +121,6 @@ public class SpotImpl extends NamedImpl implements Spot { } private URL getMeasurementsUrl() throws MalformedURLException { - // TODO this hasn't been aligned with WindFinder yet; so far we're seeing "random" filenames in the URLs such as sap_schilksee_10044N.json... -// return new URL("http://external.windfinder.com/sap_"+getId()+".json"); - return new URL("http://external.windfinder.com/sap_schilksee_10044N.json"); + return new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+collection.getId()+"_"+getId()+".json"); } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index 3e36dda8f79..080030c3601 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -15,6 +15,7 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; import com.sap.sailing.domain.windfinderadapter.Spot; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; @@ -49,15 +50,16 @@ public class WindFinderReportParser { return result; } - Spot parseSpot(JSONObject jsonOfSingleSpot) { + Spot parseSpot(JSONObject jsonOfSingleSpot, ReviewedSpotsCollection reviewedSpotsCollection) { return new SpotImpl(jsonOfSingleSpot.get("n").toString(), jsonOfSingleSpot.get("id").toString(), jsonOfSingleSpot.get("kw").toString(), + jsonOfSingleSpot.get("c").toString(), new DegreePosition(((Number) jsonOfSingleSpot.get("lat")).doubleValue(), - ((Number) jsonOfSingleSpot.get("lon")).doubleValue()), this); + ((Number) jsonOfSingleSpot.get("lon")).doubleValue()), this, reviewedSpotsCollection); } - Iterable parseSpots(JSONArray jsonOfMultipleSpots) { - return Util.map(jsonOfMultipleSpots, jsonOfSingleSpot->parseSpot((JSONObject) jsonOfSingleSpot)); + Iterable parseSpots(JSONArray jsonOfMultipleSpots, ReviewedSpotsCollection reviewedSpotsCollection) { + return Util.map(jsonOfMultipleSpots, jsonOfSingleSpot->parseSpot((JSONObject) jsonOfSingleSpot, reviewedSpotsCollection)); } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java index c9c45f843b1..0e940d29261 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java @@ -1,19 +1,25 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.WindTracker; import com.sap.sailing.domain.tracking.WindTrackerFactory; +import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; public class WindFinderTrackerFactory implements WindTrackerFactory { private final Map windTrackerPerRace; + private final Set reviewedSpotsCollections; public WindFinderTrackerFactory() { - windTrackerPerRace = new HashMap<>(); + this.windTrackerPerRace = new HashMap<>(); + this.reviewedSpotsCollections = Collections.synchronizedSet(new HashSet<>()); } @Override @@ -24,7 +30,7 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { final WindTracker existingWindTrackerForRace = getExistingWindTracker(race); if (existingWindTrackerForRace == null) { final DynamicTrackedRace trackedRace = trackedRegatta.getTrackedRace(race); - result = new WindFinderWindTracker(trackedRace, correctByDeclination, this); + result = new WindFinderWindTracker(trackedRace, this); windTrackerPerRace.put(race, result); } else { result = existingWindTrackerForRace; @@ -46,4 +52,22 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { } } + /** + * Obtains the reviewed spot collections that this factory knows about. This set is + * configurable using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and + * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}. + * + * @return an unmodifiable set of spots collections known by this factory + */ + public Iterable getReviewedSpotsCollections() { + return Collections.unmodifiableSet(reviewedSpotsCollections); + } + + public void addReviewedSpotCollection(ReviewedSpotsCollection collection) { + reviewedSpotsCollections.add(collection); + } + + public void removeReviewedSpotCollection(ReviewedSpotsCollection collection) { + reviewedSpotsCollections.remove(collection); + } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index f2cf5e5cfff..90f614c9702 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -1,19 +1,25 @@ package com.sap.sailing.domain.windfinderadapter.impl; -import java.util.Collections; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import org.json.simple.parser.ParseException; + import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.WindTracker; +import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; import com.sap.sailing.domain.windfinderadapter.Spot; import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; import com.sap.sse.util.ThreadPoolUtil; /** @@ -37,17 +43,13 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private static final Duration POLL_EVERY = Duration.ONE_MINUTE; private final DynamicTrackedRace trackedRace; - private final boolean correctByDeclination; private final WindFinderTrackerFactory factory; - private final WindFinderReportParser parser; private ScheduledFuture poller; - public WindFinderWindTracker(DynamicTrackedRace trackedRace, boolean correctByDeclination, WindFinderTrackerFactory factory) { + public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactory factory) { this.trackedRace = trackedRace; - this.correctByDeclination = correctByDeclination; this.factory = factory; - this.parser = new WindFinderReportParser(); this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this, /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS); } @@ -70,19 +72,12 @@ public class WindFinderWindTracker implements WindTracker, Runnable { } } - private Iterable getUsefulSpots() { - // TODO Auto-generated method stub - // mocking with the single spot we also have mocked the spot measurement URL for: -// { -// "c": "Germany", -// "lon": 10.28, -// "n": "Kiel/Leuchtturm", -// "kw": "kiel_leuchtturm", -// "lat": 54.47, -// "has": "1111010", -// "id": "10044N" -// } - return Collections.singleton(new SpotImpl("Kiel/Leuchtturm", "10044N", "kiel_leuchtturm", new DegreePosition(54.47, 10.28), parser)); + private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException { + final Set spots = new HashSet<>(); + for (final ReviewedSpotsCollection collection : factory.getReviewedSpotsCollections()) { + Util.addAll(collection.getSpots(), spots); + } + return spots; } @Override From 6a31b5fb76e17752a5c6d88f59d7c8eedf809bde Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 4 Jan 2018 18:07:34 +0100 Subject: [PATCH 019/214] added wind finder reviewed spot collections IDs to Event; including persistence and DTO, but not yet Admin UI Change-Id: I1433ca1f183872fc02c72adc986bfe19c86198c3 --- ...nderReviewedSpotsCollectionIdProvider.java | 17 +++++++++ .../impl/DomainObjectFactoryImpl.java | 5 +++ .../domain/persistence/impl/FieldNames.java | 1 + .../impl/MongoObjectFactoryImpl.java | 5 +++ .../META-INF/MANIFEST.MF | 3 +- .../impl/WindFinderTrackerFactory.java | 35 +++++++++++++++++-- .../impl/WindFinderWindTracker.java | 3 +- .../com/sap/sailing/domain/base/Event.java | 14 ++++++++ .../sailing/domain/base/impl/EventImpl.java | 18 ++++++++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 3 +- .../gwt/ui/server/SailingServiceImpl.java | 1 + .../sap/sailing/gwt/ui/shared/EventDTO.java | 31 ++++++++++++++++ .../sailing/server/RacingEventService.java | 3 +- .../sap/sailing/server/impl/Activator.java | 2 ++ .../server/impl/RacingEventServiceImpl.java | 8 +++++ 15 files changed, 143 insertions(+), 6 deletions(-) create mode 100755 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindFinderReviewedSpotsCollectionIdProvider.java diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindFinderReviewedSpotsCollectionIdProvider.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindFinderReviewedSpotsCollectionIdProvider.java new file mode 100755 index 00000000000..86980e5f00b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindFinderReviewedSpotsCollectionIdProvider.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.common; + +/** + * Something that can provide IDs of WindFinder (https://www.windfinder.com) spot collections + * that have been reviewed by WindFinder and have been released for use. Typically, an event + * would specify zero or more such {@link String} IDs, the {@code RacingEventService} would + * expose the result of collecting those over all events through this interface, and the + * WindFinder adapter that is responsible for receiving wind data through the WindFinder API + * would request them through this interface that it expects to discover through the OSGi + * registry. + * + * @author Axel Uhl (D043530) + * + */ +public interface WindFinderReviewedSpotsCollectionIdProvider { + Iterable getWindFinderReviewedSpotsCollectionIds(); +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 3753f48e93e..efed552b20c 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -1058,6 +1058,11 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { Venue venue = loadVenue((DBObject) eventDBObject.get(FieldNames.VENUE.name())); Event result = new EventImpl(name, startDate, endDate, venue, isPublic, id); result.setDescription(description); + @SuppressWarnings("unchecked") + final List windFinderReviewedSpotCollectionIds = (List) eventDBObject.get(FieldNames.EVENT_WINDFINDER_SPOT_COLLECTION_IDS.name()); + if (windFinderReviewedSpotCollectionIds != null) { + result.setWindFinderReviewedSpotsCollection(windFinderReviewedSpotCollectionIds); + } String officialWebSiteURLAsString = (String) eventDBObject.get(FieldNames.EVENT_OFFICIAL_WEBSITE_URL.name()); if (officialWebSiteURLAsString != null) { try { diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java index a3dfe0b6343..54efc610203 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java @@ -26,6 +26,7 @@ public enum FieldNames { EVENT_SAILORS_INFO_WEBSITE_URL, EVENT_SAILORS_INFO_WEBSITES, EVENT_LOGO_IMAGE_URL, + EVENT_WINDFINDER_SPOT_COLLECTION_IDS, REGATTA_NAME, REGATTA_ID, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 68de78cdd2b..94b46b0451c 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -594,6 +594,11 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); + BasicDBList windFinderSpotCollectionIds = new BasicDBList(); + for (final String windFinderSpotCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { + windFinderSpotCollectionIds.add(windFinderSpotCollectionId); + } + eventDBObject.put(FieldNames.EVENT_WINDFINDER_SPOT_COLLECTION_IDS.name(), windFinderSpotCollectionIds); DBObject venueDBObject = getVenueAsDBObject(event.getVenue()); eventDBObject.put(FieldNames.VENUE.name(), venueDBObject); BasicDBList images = new BasicDBList(); diff --git a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF index a075b588a39..16088fed7c6 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF @@ -6,7 +6,8 @@ Bundle-Version: 1.0.0.qualifier Bundle-Activator: com.sap.sailing.domain.windfinderadapter.impl.Activator Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.osgi.framework;version="1.3.0" +Import-Package: org.osgi.framework;version="1.3.0", + org.osgi.util.tracker;version="1.5.1" Bundle-ActivationPolicy: lazy Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sap.sailing.domain.common, diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java index 0e940d29261..3f67eb52e63 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java @@ -6,20 +6,38 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.osgi.util.tracker.ServiceTracker; + import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.WindTracker; import com.sap.sailing.domain.tracking.WindTrackerFactory; import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; +import com.sap.sse.common.Util; +import com.sap.sse.util.ServiceTrackerFactory; public class WindFinderTrackerFactory implements WindTrackerFactory { private final Map windTrackerPerRace; - private final Set reviewedSpotsCollections; + /** + * Can hold reviewed spots collections. Starts out as {@code null}. If this is the case, an attempt + * will be made to retrieve spot collection IDs from {@link #reviewedSpotsCollectionIdProvider} if + * that is not {@code null} either, e.g., because we're running outside of an OSGi environment such + * as a test set-up. + */ + private Set reviewedSpotsCollections; + private final ServiceTracker reviewedSpotsCollectionIdProvider; + public WindFinderTrackerFactory() { this.windTrackerPerRace = new HashMap<>(); this.reviewedSpotsCollections = Collections.synchronizedSet(new HashSet<>()); + if (Activator.getContext() != null) { + reviewedSpotsCollectionIdProvider = ServiceTrackerFactory.createAndOpen(Activator.getContext(), WindFinderReviewedSpotsCollectionIdProvider.class); + } else { + reviewedSpotsCollectionIdProvider = null; + } } @Override @@ -60,7 +78,20 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { * @return an unmodifiable set of spots collections known by this factory */ public Iterable getReviewedSpotsCollections() { - return Collections.unmodifiableSet(reviewedSpotsCollections); + final Iterable result; + if (reviewedSpotsCollections != null) { + result = Collections.unmodifiableSet(reviewedSpotsCollections); + } else { + final WindFinderReviewedSpotsCollectionIdProvider provider; + if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) { + reviewedSpotsCollections = new HashSet<>(); + Util.addAll(Util.map(provider.getWindFinderReviewedSpotsCollectionIds(), id->new ReviewedSpotsCollectionImpl(id)), reviewedSpotsCollections); + result = Collections.unmodifiableSet(reviewedSpotsCollections); + } else { + result = Collections.emptySet(); + } + } + return result; } public void addReviewedSpotCollection(ReviewedSpotsCollection collection) { diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index 90f614c9702..4dcaa49954b 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -46,7 +46,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private final WindFinderTrackerFactory factory; private ScheduledFuture poller; - + public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactory factory) { this.trackedRace = trackedRace; this.factory = factory; @@ -75,6 +75,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException { final Set spots = new HashSet<>(); for (final ReviewedSpotsCollection collection : factory.getReviewedSpotsCollections()) { + // TODO bug1301 judge each spot's usefulness given the location of trackedRace Util.addAll(collection.getSpots(), spots); } return spots; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java index 071b0bce01a..b7da4927637 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java @@ -41,4 +41,18 @@ public interface Event extends EventBase { * leaderboard groups in leaderboardGroups. */ void setLeaderboardGroups(Iterable leaderboardGroups); + + /** + * An event may happen in the vicinity of one or more WindFinder (https://www.windfinder.com) weather + * stations. Which ones those are can be defined using {@link #setWindFinderReviewedSpotsCollection(Iterable)}, + * and this getter returns the IDs last set. + */ + Iterable getWindFinderReviewedSpotsCollectionIds(); + + /** + * Set the IDs of the reviewed WindFinder spot collections to consider during this event. + * Setting this to a non-empty value shall lead to a corresponding display of a WindFinder + * logo / link on the event's UI representation. + */ + void setWindFinderReviewedSpotsCollection(Iterable reviewedSpotsCollectionIds); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java index cf7c0044f97..df08dad6ac2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java @@ -4,7 +4,9 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.util.Collections; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.base.Venue; @@ -17,6 +19,8 @@ public class EventImpl extends EventBaseImpl implements Event { private ConcurrentLinkedQueue leaderboardGroups; + private final ConcurrentMap windFinderReviewedSpotsCollectionIds; + public EventImpl(String name, TimePoint startDate, TimePoint endDate, String venueName, boolean isPublic, UUID id) { this(name, startDate, endDate, new VenueImpl(venueName), isPublic, id); } @@ -27,6 +31,7 @@ public class EventImpl extends EventBaseImpl implements Event { public EventImpl(String name, TimePoint startDate, TimePoint endDate, Venue venue, boolean isPublic, UUID id) { super(name, startDate, endDate, venue, isPublic, id); this.leaderboardGroups = new ConcurrentLinkedQueue<>(); + this.windFinderReviewedSpotsCollectionIds = new ConcurrentHashMap<>(); } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { @@ -60,4 +65,17 @@ public class EventImpl extends EventBaseImpl implements Event { public boolean removeLeaderboardGroup(LeaderboardGroup leaderboardGroup) { return leaderboardGroups.remove(leaderboardGroup); } + + @Override + public Iterable getWindFinderReviewedSpotsCollectionIds() { + return Collections.unmodifiableSet(windFinderReviewedSpotsCollectionIds.keySet()); + } + + @Override + public void setWindFinderReviewedSpotsCollection(Iterable reviewedSpotsCollectionIds) { + windFinderReviewedSpotsCollectionIds.clear(); + for (final String reviewedSpotsCollectionId : reviewedSpotsCollectionIds) { + windFinderReviewedSpotsCollectionIds.putIfAbsent(reviewedSpotsCollectionId, true); + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 2a4a988c507..95dbe305386 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -878,7 +878,8 @@ public class RaceMap extends AbstractCompositeComponent impleme WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); switch (windSource.getType()) { case EXPEDITION: - // we filter out measured wind sources with vary low confidence + case WINDFINDER: + // we filter out measured wind sources with very low confidence if (windTrackInfoDTO.minWindConfidence > 0.0001) { windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 1f5a127097e..2122315b73b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3914,6 +3914,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (LeaderboardGroup lg : event.getLeaderboardGroups()) { eventDTO.addLeaderboardGroup(convertToLeaderboardGroupDTO(lg, /* withGeoLocationData */false, withStatisticalData)); } + eventDTO.setWindFinderReviewedSpotsCollection(event.getWindFinderReviewedSpotsCollectionIds()); return eventDTO; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java index 070f415a7da..cf11b44a3c5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java @@ -1,11 +1,13 @@ package com.sap.sailing.gwt.ui.shared; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.UUID; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; +import com.sap.sse.common.Util; public class EventDTO extends EventBaseDTO { private static final long serialVersionUID = -7100030301376959817L; @@ -13,6 +15,8 @@ public class EventDTO extends EventBaseDTO { private Date currentServerTime; private List leaderboardGroups; // keeps the more specific type accessible in a type-safe way + + private List windFinderReviewedSpotsCollectionIds; public EventDTO() { this(new ArrayList()); @@ -80,5 +84,32 @@ public class EventDTO extends EventBaseDTO { } return null; } + + /** + * An event may happen in the vicinity of one or more WindFinder (https://www.windfinder.com) weather + * stations. Which ones those are can be defined using {@link #setWindFinderReviewedSpotsCollection(Iterable)}, + * and this getter returns the IDs last set. + * + * @return always a valid {@link Iterable} which may, though, be empty + */ + public Iterable getWindFinderReviewedSpotsCollection() { + final Iterable result; + if (windFinderReviewedSpotsCollectionIds == null) { + result = Collections.emptySet(); + } else { + result = windFinderReviewedSpotsCollectionIds; + } + return result; + } + + /** + * Set the IDs of the reviewed WindFinder spot collections to consider during this event. + * Setting this to a non-empty value shall lead to a corresponding display of a WindFinder + * logo / link on the event's UI representation. + */ + public void setWindFinderReviewedSpotsCollection(Iterable reviewedSpotsCollectionId) { + windFinderReviewedSpotsCollectionIds = new ArrayList<>(); + Util.addAll(reviewedSpotsCollectionId, windFinderReviewedSpotsCollectionIds); + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 758fcad562a..944f4b32afc 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -55,6 +55,7 @@ import com.sap.sailing.domain.common.RegattaFetcher; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider; import com.sap.sailing.domain.common.dto.AnniversaryType; import com.sap.sailing.domain.common.media.MediaTrack; import com.sap.sailing.domain.common.racelog.RacingProcedureType; @@ -132,7 +133,7 @@ import com.sap.sse.shared.media.VideoDescriptor; public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetcher, RegattaRegistry, RaceFetcher, LeaderboardRegistry, EventResolver, LeaderboardGroupResolver, TrackerManager, Searchable, ReplicableWithObjectInputStream>, RaceLogResolver, - SensorFixStoreSupplier { + SensorFixStoreSupplier, WindFinderReviewedSpotsCollectionIdProvider { @Override Regatta getRegatta(RegattaName regattaName); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index 3d8bb6275d3..5296fd28d91 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -22,6 +22,7 @@ import org.osgi.util.tracker.ServiceTrackerCustomizer; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider; import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; @@ -141,6 +142,7 @@ public class Activator implements BundleActivator { context.registerService(RaceLogResolver.class, racingEventService, null); context.registerService(ClearStateTestSupport.class, racingEventService, null); context.registerService(SensorFixStoreSupplier.class, racingEventService, null); + context.registerService(WindFinderReviewedSpotsCollectionIdProvider.class, racingEventService, null); Dictionary properties = new Hashtable(); final GPSFixMongoHandlerImpl gpsFixMongoHandler = new GPSFixMongoHandlerImpl( racingEventService.getMongoObjectFactory(), racingEventService.getDomainObjectFactory()); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 0adfb11669b..a6dc14d83a9 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -4223,4 +4223,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes return pairingList; } + @Override + public Iterable getWindFinderReviewedSpotsCollectionIds() { + final Set result = new HashSet<>(); + for (final Event event : getAllEvents()) { + Util.addAll(event.getWindFinderReviewedSpotsCollectionIds(), result); + } + return result; + } } From ec335351124461e13ebf1731bfb6be2a244e57d8 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 11:20:36 +0100 Subject: [PATCH 020/214] added WindFinder spot collection ID to event admin UI Change-Id: I53b5067501c498dc138354bf929df9fce3640cae --- .../gwt/ui/adminconsole/EventDialog.java | 9 +++ .../gwt/ui/adminconsole/EventEditDialog.java | 1 + .../ui/adminconsole/EventListComposite.java | 76 +++++++++---------- .../gwt/ui/adminconsole/ImageDialog.java | 2 +- .../gwt/ui/adminconsole/VideoDialog.java | 2 +- .../sailing/gwt/ui/client/SailingService.java | 5 +- .../gwt/ui/client/SailingServiceAsync.java | 2 +- .../sailing/gwt/ui/client/StringMessages.java | 4 + .../gwt/ui/client/StringMessages.properties | 6 +- .../ui/client/StringMessages_de.properties | 6 +- .../gwt/ui/server/SailingServiceImpl.java | 9 ++- .../gateway/jaxrs/api/EventsResource.java | 4 +- .../impl/ExpeditionAllInOneImporter.java | 2 +- .../SailingServer (No Proxy).launch | 2 +- .../sailing/server/RacingEventService.java | 7 +- .../server/impl/RacingEventServiceImpl.java | 4 +- .../UpdateEvent.java | 7 +- .../GenericStringListEditorComposite.java | 7 +- 18 files changed, 98 insertions(+), 57 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java index fe302b6d673..48376b38845 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java @@ -34,6 +34,7 @@ import com.sap.sse.gwt.client.IconResources; import com.sap.sse.gwt.client.controls.datetime.DateAndTimeInput; import com.sap.sse.gwt.client.controls.listedit.GenericStringListInlineEditorComposite; import com.sap.sse.gwt.client.controls.listedit.StringConstantsListEditorComposite; +import com.sap.sse.gwt.client.controls.listedit.StringListInlineEditorComposite; import com.sap.sse.gwt.client.media.ImageDTO; import com.sap.sse.gwt.client.media.VideoDTO; @@ -49,6 +50,7 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox availableLeaderboardGroupsByName; protected ImagesListComposite imagesListComposite; protected VideosListComposite videosListComposite; @@ -160,6 +162,9 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox emptyList(), + new GenericStringListInlineEditorComposite.ExpandedUi(stringMessages, IconResources.INSTANCE.removeIcon(), /* suggestValues */ + Collections.emptyList(), stringMessages.enterIdOfWindfinderReviewedSpotCollection(), 80)); } @Override @@ -189,6 +194,7 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox() { + updatedEvent.getVideos(), updatedEvent.getWindFinderReviewedSpotsCollection(), new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to update sailing event" + oldEvent.getName() + ": " + t.getMessage()); + } + + @Override + public void onSuccess(EventDTO result) { + fillEvents(); + final String[] namesOfCourseAreasToAdd = new String[courseAreasToAdd.size()]; + int i=0; + for (CourseAreaDTO courseAreaToAdd : courseAreasToAdd) { + namesOfCourseAreasToAdd[i++] = courseAreaToAdd.getName(); + } + sailingService.createCourseAreas(oldEvent.id, namesOfCourseAreasToAdd, new AsyncCallback() { @Override public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to update sailing event" + oldEvent.getName() + ": " + t.getMessage()); + errorReporter.reportError("Error trying to add course area to sailing event " + oldEvent.getName() + + ": " + t.getMessage()); } @Override - public void onSuccess(EventDTO result) { - fillEvents(); - final String[] namesOfCourseAreasToAdd = new String[courseAreasToAdd.size()]; - int i=0; - for (CourseAreaDTO courseAreaToAdd : courseAreasToAdd) { - namesOfCourseAreasToAdd[i++] = courseAreaToAdd.getName(); + public void onSuccess(Void result) { + final UUID[] idsOfCourseAreasToRemove = new UUID[courseAreasToRemove.size()]; + int j=0; + for (CourseAreaDTO courseAreaToRemove : courseAreasToRemove) { + idsOfCourseAreasToRemove[j++] = courseAreaToRemove.id; } - sailingService.createCourseAreas(oldEvent.id, namesOfCourseAreasToAdd, new AsyncCallback() { + sailingService.removeCourseAreas(oldEvent.id, idsOfCourseAreasToRemove, new AsyncCallback() { @Override public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to add course area to sailing event " + oldEvent.getName() + errorReporter.reportError("Error trying to remove course area from sailing event " + oldEvent.getName() + ": " + t.getMessage()); } @Override public void onSuccess(Void result) { - final UUID[] idsOfCourseAreasToRemove = new UUID[courseAreasToRemove.size()]; - int j=0; - for (CourseAreaDTO courseAreaToRemove : courseAreasToRemove) { - idsOfCourseAreasToRemove[j++] = courseAreaToRemove.id; - } - sailingService.removeCourseAreas(oldEvent.id, idsOfCourseAreasToRemove, new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to remove course area from sailing event " + oldEvent.getName() - + ": " + t.getMessage()); - } - - @Override - public void onSuccess(Void result) { - fillEvents(); - if (!oldEvent.getName().equals(updatedEvent.getName())) { - sailingService.renameEvent(oldEvent.id, updatedEvent.getName(), new AsyncCallback() { - @Override - public void onSuccess(Void result) { - } - - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to rename sailing event " + oldEvent.getName() + ": " + t.getMessage()); - } - }); + fillEvents(); + if (!oldEvent.getName().equals(updatedEvent.getName())) { + sailingService.renameEvent(oldEvent.id, updatedEvent.getName(), new AsyncCallback() { + @Override + public void onSuccess(Void result) { } - } - }); + + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to rename sailing event " + oldEvent.getName() + ": " + t.getMessage()); + } + }); + } } }); } }); + } + }); } private Pair, List> getCourseAreasToAdd(final EventDTO oldEvent, final EventDTO updatedEvent) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImageDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImageDialog.java index f1faf70e17a..0bd7c3f1d6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImageDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImageDialog.java @@ -141,7 +141,7 @@ public abstract class ImageDialog extends DataEntryDialog { tagsListEditor = new StringListInlineEditorComposite(Collections. emptyList(), new GenericStringListInlineEditorComposite.ExpandedUi(stringMessages, IconResources.INSTANCE.removeIcon(), /* suggestValues */ - MediaConstants.imageTagSuggestions, "Enter tags for the image", 30)); + MediaConstants.imageTagSuggestions, stringMessages.enterTagsForTheImage(), 30)); tagsListEditor.addValueChangeHandler(new ValueChangeHandler>() { @Override public void onValueChange(ValueChangeEvent> event) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java index b89cfc219b6..c74366727cf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java @@ -92,7 +92,7 @@ public abstract class VideoDialog extends DataEntryDialog { thumbnailURLAndUploadComposite = new URLFieldWithFileUpload(stringMessages); tagsListEditor = new StringListInlineEditorComposite(Collections. emptyList(), new GenericStringListInlineEditorComposite.ExpandedUi(stringMessages, IconResources.INSTANCE.removeIcon(), /* suggestValues */ - MediaConstants.videoTagSuggestions, "Enter tags for the video", 50)); + MediaConstants.videoTagSuggestions, stringMessages.enterTagsForTheVideo(), 50)); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 5ee4920d4b3..35d860f6bc7 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -317,8 +317,9 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS Pair resolveImageDimensions(String imageUrlAsString) throws Exception; EventDTO updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, - VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, String baseURLAsString, - Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos) throws Exception; + VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, + String baseURL, Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, + Iterable videos, Iterable windFinderReviewedSpotCollectionIds) throws Exception; EventDTO createEvent(String eventName, String eventDescription, Date startDate, Date endDate, String venue, boolean isPublic, List courseAreaNames, String officialWebsiteURL, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index c179402ea0e..e9e2f1ea440 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -428,7 +428,7 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, String baseURL, Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, - Iterable videos, AsyncCallback callback); + Iterable videos, Iterable windFinderReviewedSpotCollectionIds, AsyncCallback callback); void resolveImageDimensions(String imageUrlAsString, AsyncCallback> callback); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index c7d76702f16..e7934835f46 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2028,4 +2028,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String chooseAName(); String windFinderWindSourceTypeName(); String windFinderWindSourceTypeTooltip(); + String windFinder(); + String enterTagsForTheVideo(); + String enterIdOfWindfinderReviewedSpotCollection(); + String enterTagsForTheImage(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 4b696c8b0b3..f8533fac056 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2016,4 +2016,8 @@ importFinishedGotoRaceboard=Open raceboard importFinishedGotoEvent=Open event chooseAName=Choose a name windFinderWindSourceTypeName=WindFinder -windFinderWindSourceTypeTooltip=Measured wind from one or more spots of www.windfinder.com \ No newline at end of file +windFinderWindSourceTypeTooltip=Measured wind from one or more spots of www.windfinder.com +windFinder=WindFinder +enterTagsForTheVideo=Enter tags for the video +enterIdOfWindfinderReviewedSpotCollection=Enter ID of a reviewed WindFinder spot collection, e.g., "schilksee" +enterTagsForTheImage=Enter tags for the image \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index ea8a7ab0181..aa02fe6bd65 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2010,4 +2010,8 @@ importFinishedGotoRaceboard=Raceboard öffnen importFinishedGotoEvent=Event öffnen chooseAName=Namen auswählen windFinderWindSourceTypeName=WindFinder -windFinderWindSourceTypeTooltip=Gemessener Wind von einer oder mehreren Stationen von www.windfinder.com \ No newline at end of file +windFinderWindSourceTypeTooltip=Gemessener Wind von einer oder mehreren Stationen von www.windfinder.com +windFinder=WindFinder +enterTagsForTheVideo=Tags zum Video erfassen +enterIdOfWindfinderReviewedSpotCollection=ID einer geprüften WindFinder Messstellen-Sammlung eingeben, z.B. "schilksee" +enterTagsForTheImage=Tags zum Bild erfassen \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 2122315b73b..f14e32c8fe9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3645,7 +3645,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public EventDTO updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURLString, String baseURLAsString, - Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos) throws MalformedURLException { + Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos, + Iterable windFinderReviewedSpotCollectionIds) throws MalformedURLException { TimePoint startTimePoint = startDate != null ? new MillisecondsTimePoint(startDate) : null; TimePoint endTimePoint = endDate != null ? new MillisecondsTimePoint(endDate) : null; URL officialWebsiteURL = officialWebsiteURLString != null ? new URL(officialWebsiteURLString) : null; @@ -3655,7 +3656,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S List eventVideos = convertToVideos(videos); getService().apply( new UpdateEvent(eventId, eventName, eventDescription, startTimePoint, endTimePoint, venue.getName(), - isPublic, leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, eventImages, eventVideos)); + isPublic, leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, eventImages, + eventVideos, windFinderReviewedSpotCollectionIds)); return getEventById(eventId, false); } @@ -4514,7 +4516,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } updateEvent(newEvent.id, newEvent.getName(), description, newEvent.startDate, newEvent.endDate, newEvent.venue, newEvent.isPublic, eventLeaderboardGroupUUIDs, newEvent.getOfficialWebsiteURL(), - newEvent.getBaseURL(), newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos()); + newEvent.getBaseURL(), newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos(), + newEvent.getWindFinderReviewedSpotsCollection()); } @Override diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java index dbe534e7865..ddf7bc694c8 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java @@ -236,7 +236,7 @@ public class EventsResource extends AbstractSailingServerResource { baseURL = baseURLParam != null ? new URL(baseURLParam) : event.getBaseURL(); getService().updateEvent(id, eventName, eventDescription, startDate, endDate, venueName, isPublic, leaderboardGroupIds, officialWebsiteURL, baseURL, event.getSailorsInfoWebsiteURLs(), - event.getImages(), event.getVideos()); + event.getImages(), event.getVideos(), event.getWindFinderReviewedSpotsCollectionIds()); response = Response.ok().build(); } return response; @@ -524,7 +524,7 @@ public class EventsResource extends AbstractSailingServerResource { getService().updateEvent(event.getId(), event.getName(), event.getDescription(), event.getStartDate(), event.getEndDate(), event.getVenue().getName(), event.isPublic(), newLeaderboardGroupIds, event.getOfficialWebsiteURL(), event.getBaseURL(), event.getSailorsInfoWebsiteURLs(), event.getImages(), - event.getVideos()); + event.getVideos(), event.getWindFinderReviewedSpotsCollectionIds()); } private CourseArea addCourseArea(UUID eventId, String courseAreaName) { diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 9a0dec2752b..4fed8d37a96 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -221,7 +221,7 @@ public class ExpeditionAllInOneImporter { service.updateEvent(event.getId(), event.getName(), event.getDescription(), event.getStartDate(), event.getEndDate(), event.getVenue().getName(), event.isPublic(), Collections.singleton(leaderboardGroup.getId()), event.getOfficialWebsiteURL(), event.getBaseURL(), - event.getSailorsInfoWebsiteURLs(), event.getImages(), event.getVideos()); + event.getSailorsInfoWebsiteURLs(), event.getImages(), event.getVideos(), event.getWindFinderReviewedSpotsCollectionIds()); final RaceColumn raceColumn = regattaLeaderboard.getRaceColumns().iterator().next(); final Fleet fleet = raceColumn.getFleets().iterator().next(); diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index 0a323bd78fe..7451056d7b2 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -36,5 +36,5 @@ - + diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 944f4b32afc..37a2756626c 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -374,6 +374,7 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch /** * Updates a sailing event with the name eventName, the venuevenue and the regattas with * the names in regattaNames and updates it in the database. + * * @param eventName * The name of the event to update * @param startDate @@ -384,11 +385,15 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch * The name of the venue of the event * @param isPublic * Indicates whether the event is public accessible via the publication URL or not + * @param windFinderReviewedSpotCollectionIds + * IDs of WindFinder spot collections, reviewed by www.windfinder.com, to use by the WindFinder wind + * tracker and to expose on the event's UI * @return The new event */ void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, - Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos); + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos, + Iterable windFinderReviewedSpotCollectionIds); /** * Renames a sailing event. If a sailing event by the name oldName does not exist in {@link #getEvents()}, diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index a6dc14d83a9..aea65c2c308 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -3178,7 +3178,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, - Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos) { + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos, + Iterable windFinderReviewedSpotCollectionIds) { final Event event = eventsById.get(id); if (event == null) { throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist."); @@ -3205,6 +3206,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes event.setSailorsInfoWebsiteURLs(sailorsInfoWebsiteURLs); event.setImages(images); event.setVideos(videos); + event.setWindFinderReviewedSpotsCollection(windFinderReviewedSpotCollectionIds); // TODO consider use diffutils to compute diff between old and new leaderboard groups list and apply the patch // to keep changes minimial mongoObjectFactory.storeEvent(event); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java index 4cabbe3c4a4..ffd879182a0 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java @@ -25,10 +25,11 @@ public class UpdateEvent extends AbstractEventOperation { private final Map sailorsInfoWebsiteURLs; private final Iterable images; private final Iterable videos; + private final Iterable windFinderReviewedSpotCollectionIds; public UpdateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, - Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos) { + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos, Iterable windFinderReviewedSpotCollectionIds) { super(id); this.eventName = eventName; this.eventDescription = eventDescription; @@ -42,6 +43,7 @@ public class UpdateEvent extends AbstractEventOperation { this.sailorsInfoWebsiteURLs = sailorsInfoWebsiteURLs; this.images = images; this.videos = videos; + this.windFinderReviewedSpotCollectionIds = windFinderReviewedSpotCollectionIds; } @Override @@ -59,7 +61,8 @@ public class UpdateEvent extends AbstractEventOperation { @Override public Void internalApplyTo(RacingEventService toState) { toState.updateEvent(getId(), eventName, eventDescription, startDate, endDate, venueName, isPublic, - leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, images, videos); + leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, images, videos, + windFinderReviewedSpotCollectionIds); return null; } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/listedit/GenericStringListEditorComposite.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/listedit/GenericStringListEditorComposite.java index 4d2b37b513f..02bfdddb31d 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/listedit/GenericStringListEditorComposite.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/listedit/GenericStringListEditorComposite.java @@ -22,7 +22,8 @@ import com.sap.sse.gwt.client.StringMessages; /** * A list editor that renders values of type {@code ValueType} as {@link String}s and lets the user edit them - * as strings. For this to work, implementing subclasses need to specify the + * as strings. For this to work, implementing subclasses need to specify how strings are parsed to objects + * of type {@code ValueType} and vice versa. * * @author Lukas Niemeier * @author Axel Uhl (d043530) @@ -93,6 +94,10 @@ public abstract class GenericStringListEditorComposite extends ListEd this(stringMessages, removeImage, suggestValues, /* placeholderTextForAddTextbox */ null); } + /** + * @param suggestValues must not be null but may be empty + * @param placeholderTextForAddTextbox may be null + */ public ExpandedUi(StringMessages stringMessages, ImageResource removeImage, Iterable suggestValues, String placeholderTextForAddTextbox) { super(stringMessages, removeImage, /*canRemoveItems*/true); this.placeholderTextForAddTextbox = placeholderTextForAddTextbox; From 9519af0f595076b1bf7f5395163ab9d9b747458c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 11:35:37 +0100 Subject: [PATCH 021/214] construct spot ID collection when the WindFinderWindTracker is constructed Change-Id: I999bcff692311e5ce721737786cd55537e0b7047 --- .../impl/ReviewedSpotsCollectionImpl.java | 4 +++ .../impl/WindFinderTrackerFactory.java | 30 ++++++++----------- .../impl/WindFinderWindTracker.java | 12 ++++++-- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java index 4e7ca9fe25c..a8091cd1e99 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java @@ -66,4 +66,8 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { return parser.parseSpots(spotsAsJson, this); } + @Override + public String toString() { + return "WindFinder Reviewed Spots Collection [id=" + id + "]"; + } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java index 3f67eb52e63..b06658d0ad3 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java @@ -1,8 +1,10 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -71,25 +73,19 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { } /** - * Obtains the reviewed spot collections that this factory knows about. This set is - * configurable using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and - * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}. + * Obtains the reviewed spot collections that this factory knows about. This set is constructed from the collections + * provided explicitly using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and + * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained + * through {@link #reviewedSpotsCollectionIdProvider} if a corresponding service can be resolved. * - * @return an unmodifiable set of spots collections known by this factory + * @return a non-live set of spots collections known by this factory at this point in time */ - public Iterable getReviewedSpotsCollections() { - final Iterable result; - if (reviewedSpotsCollections != null) { - result = Collections.unmodifiableSet(reviewedSpotsCollections); - } else { - final WindFinderReviewedSpotsCollectionIdProvider provider; - if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) { - reviewedSpotsCollections = new HashSet<>(); - Util.addAll(Util.map(provider.getWindFinderReviewedSpotsCollectionIds(), id->new ReviewedSpotsCollectionImpl(id)), reviewedSpotsCollections); - result = Collections.unmodifiableSet(reviewedSpotsCollections); - } else { - result = Collections.emptySet(); - } + Iterable getReviewedSpotsCollections() { + final List result = new ArrayList<>(); + result.addAll(reviewedSpotsCollections); + final WindFinderReviewedSpotsCollectionIdProvider provider; + if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) { + Util.addAll(Util.map(provider.getWindFinderReviewedSpotsCollectionIds(), id->new ReviewedSpotsCollectionImpl(id)), result); } return result; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index 4dcaa49954b..7735cef1e2b 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -45,11 +45,19 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private final DynamicTrackedRace trackedRace; private final WindFinderTrackerFactory factory; - private ScheduledFuture poller; + private final ScheduledFuture poller; + + /** + * The set of all {@link ReviewedSpotsCollection}s delivered by the {@link #factory} when this tracker + * was created. This is the basis for {@link #getUsefulSpots()} when evaluating, e.g., based on the + * {@link #trackedRace race's} location, which of the spots is actually useful for this tracker. + */ + private final Iterable allSpotCollections; public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactory factory) { this.trackedRace = trackedRace; this.factory = factory; + this.allSpotCollections = factory.getReviewedSpotsCollections(); this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this, /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS); } @@ -74,7 +82,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException { final Set spots = new HashSet<>(); - for (final ReviewedSpotsCollection collection : factory.getReviewedSpotsCollections()) { + for (final ReviewedSpotsCollection collection : allSpotCollections) { // TODO bug1301 judge each spot's usefulness given the location of trackedRace Util.addAll(collection.getSpots(), spots); } From bd60f39e0ec89382ea6c50d76be2a31543a9344a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 15:48:05 +0100 Subject: [PATCH 022/214] use degrees and decimal minutes for position displays Change-Id: I8ae9f32f522a13801b10d2c208d4cbbbb88c4de9 --- .../org.eclipse.core.resources.prefs | 1 + .../domain/common/AbstractPosition.java | 25 ++++++++ .../sap/sailing/domain/common/Position.java | 10 ++++ .../org.eclipse.core.resources.prefs | 1 + .../sap/sailing/domain/test/PositionTest.java | 60 +++++++++++++++++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 15 ++--- .../sse/common/test/NumberPaddingTest.java | 49 +++++++++++++++ .../src/com/sap/sse/common/Util.java | 37 ++++++++++++ 8 files changed, 188 insertions(+), 10 deletions(-) create mode 100755 java/com.sap.sse.common.test/src/com/sap/sse/common/test/NumberPaddingTest.java diff --git a/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs index f96e639d740..f45ef9dc346 100755 --- a/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs @@ -1,5 +1,6 @@ eclipse.preferences.version=1 encoding//src/com/sap/sailing/domain/common/AbstractBearing.java=UTF-8 +encoding//src/com/sap/sailing/domain/common/AbstractPosition.java=UTF-8 encoding//src/com/sap/sailing/domain/common/LegType.java=UTF-8 encoding//src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java=utf-8 encoding//src/com/sap/sailing/domain/common/impl/AbstractSpeedWithAbstractBearingImpl.java=UTF-8 diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java index 6e81590d215..60df2d1d938 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java @@ -5,6 +5,8 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.RadianBearingImpl; import com.sap.sailing.domain.common.impl.RadianPosition; +import com.sap.sse.common.Util; +import com.sap.sse.common.util.RoundingUtil; public class AbstractPosition implements Position { private static final long serialVersionUID = -3057027562787541064L; @@ -243,4 +245,27 @@ public class AbstractPosition implements Position { return greatCircle; } + @Override + public String getAsDegreesAndDecimalMinutesWithCardinalPoints() { + final String lat = (getLatDeg()>=0 ? "N" : "S") + getDegreesAndDecimalMinutesOfNonNegativeAngle(Math.abs(getLatDeg()), /* degreePlaces */ 2, /* minuteDecimals */ 3); + final String lng = (getLngDeg()>=0 ? "E" : "W") + getDegreesAndDecimalMinutesOfNonNegativeAngle(Math.abs(getLngDeg()), /* degreePlaces */ 3, /* minuteDecimals */ 3); + return lat+" "+lng; + } + + private String getDegreesAndDecimalMinutesOfNonNegativeAngle(double nonNegativeAngle, int degreePlaces, int minuteDecimals) { + final double abs = Math.abs(nonNegativeAngle); + int integerDegrees = (int) nonNegativeAngle; + double minutes = RoundingUtil.format((abs-(int) abs)*60.0, 3); + if (minutes >= 60.0) { + minutes -= 60.0; + integerDegrees++; + } + return Util.padPositiveValue(integerDegrees, degreePlaces, 0, /* round */ true) + "°"+ + Util.padPositiveValue(minutes, 2, minuteDecimals, /* round */ true)+"'"; + } + + @Override + public String getAsSignedDecimalDegrees() { + return "("+getLatDeg()+", "+getLngDeg()+")"; + } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java index c4ef50b51aa..2d0d97b7bd2 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java @@ -11,6 +11,16 @@ public interface Position extends Serializable { double getLatDeg(); double getLngDeg(); + + /** + * Generates a string representation of the form S03°45.235' E003°22.837' + */ + String getAsDegreesAndDecimalMinutesWithCardinalPoints(); + + /** + * Generates a string representation of the form (-3.7539166666, 3.3806166666) + */ + String getAsSignedDecimalDegrees(); /** * Central angle between this position and the other position, in radians diff --git a/java/com.sap.sailing.domain.test/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.domain.test/.settings/org.eclipse.core.resources.prefs index c3595ce3c47..cd07ac442b4 100755 --- a/java/com.sap.sailing.domain.test/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.domain.test/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,4 @@ eclipse.preferences.version=1 +encoding//src/com/sap/sailing/domain/test/PositionTest.java=UTF-8 encoding//src/com/sap/sailing/domain/test/WindTest.java=UTF-8 encoding//src/com/sap/sailing/domain/test/markpassing/StarIDM2013MarkPassingTest.java=UTF-8 diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PositionTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PositionTest.java index 385f92a4a8e..00105e03aa1 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PositionTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PositionTest.java @@ -360,4 +360,64 @@ public class PositionTest { assertEquals(-Math.sqrt(1./2.), rotatedBy90DegreesCounterClockWise.getLatDeg(), 0.001); assertEquals(Math.sqrt(1./2.), rotatedBy90DegreesCounterClockWise.getLngDeg(), 0.001); } + + @Test + public void testStringRepresentation1() { + Position p = new DegreePosition(0, 0); + assertEquals("N00°00.000' E000°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation2() { + Position p = new DegreePosition(1, 1); + assertEquals("N01°00.000' E001°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation3() { + Position p = new DegreePosition(-1, -1); + assertEquals("S01°00.000' W001°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation4() { + Position p = new DegreePosition(-11, -11); + assertEquals("S11°00.000' W011°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation5() { + Position p = new DegreePosition(-11, -111); + assertEquals("S11°00.000' W111°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation6() { + Position p = new DegreePosition(-11.0 - 1.05 / 60.0, -111.0 - 1.05 / 60.0); + assertEquals("S11°01.050' W111°01.050'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation7() { + Position p = new DegreePosition(1.5, 1.5); + assertEquals("N01°30.000' E001°30.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation8() { + Position p = new DegreePosition(59.999999999/60.0, 59.999999999/60.0); + assertEquals("N01°00.000' E001°00.000'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation9() { + Position p = new DegreePosition(54.569283, 10.005400); + assertEquals("N54°34.157' E010°00.324'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } + + @Test + public void testStringRepresentation10() { + Position p = new DegreePosition(53.569127762780646, 10.004243641469529); + assertEquals("N53°34.148' E010°00.255'", p.getAsDegreesAndDecimalMinutesWithCardinalPoints()); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 95dbe305386..357e3fd0394 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2131,18 +2131,12 @@ public class RaceMap extends AbstractCompositeComponent impleme infoWindow.open(map); } - private String formatPosition(double lat, double lng) { - NumberFormat numberFormat = NumberFormat.getFormat("0.00000"); - String result = numberFormat.format(lat) + " lat, " + numberFormat.format(lng) + " lng"; - return result; - } - private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { WindSource windSource = windSensorOverlay.getWindSource(); WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - if(windDTO != null && windDTO.position != null) { - if(lastInfoWindow != null) { + if (windDTO != null && windDTO.position != null) { + if (lastInfoWindow != null) { lastInfoWindow.close(); } LatLng where = coordinateSystem.toLatLng(windDTO.position); @@ -2176,7 +2170,7 @@ public class RaceMap extends AbstractCompositeComponent impleme private Widget getInfoWindowContent(MarkDTO markDTO) { VerticalPanel vPanel = new VerticalPanel(); vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(markDTO.position.getLatDeg(), markDTO.position.getLngDeg()))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), markDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); return vPanel; } @@ -2211,7 +2205,8 @@ public class RaceMap extends AbstractCompositeComponent impleme vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(windDTO.position.getLatDeg(), windDTO.position.getLngDeg()))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsSignedDecimalDegrees())); return vPanel; } diff --git a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/NumberPaddingTest.java b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/NumberPaddingTest.java new file mode 100755 index 00000000000..149dc0021de --- /dev/null +++ b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/NumberPaddingTest.java @@ -0,0 +1,49 @@ +package com.sap.sse.common.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import static com.sap.sse.common.Util.padPositiveValue; + +public class NumberPaddingTest { + @Test + public void testValueLessThanOne() { + assertEquals(".1", padPositiveValue(0.1, 0, 1, /* round */ false)); + } + + @Test + public void testValueGreaterThanOne() { + assertEquals("1.1", padPositiveValue(1.1, 0, 1, /* round */ false)); + } + + @Test + public void testValueGreaterThanTen() { + assertEquals("10.1", padPositiveValue(10.1, 0, 1, /* round */ false)); + } + + @Test + public void testTruncation() { + assertEquals("10.1", padPositiveValue(10.179, 0, 1, /* round */ false)); + } + + @Test + public void testRounding() { + assertEquals("10.2", padPositiveValue(10.179, 0, 1, /* round */ true)); + } + + @Test + public void testRoundingCloseToInteger() { + assertEquals("11.0", padPositiveValue(10.9999999999, 0, 1, /* round */ true)); + assertEquals("11.0", padPositiveValue(11.0000000001, 0, 1, /* round */ true)); + } + + @Test + public void testLeftPad() { + assertEquals("0010.2", padPositiveValue(10.179, 4, 1, /* round */ true)); + } + + @Test + public void testRightPad() { + assertEquals("10.1790", padPositiveValue(10.179, 0, 4, /* round */ true)); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java index bb941469664..7d0e5d0a476 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -780,4 +780,41 @@ public class Util { } return toStringOrNull.toString(); } + + /** + * Pads a numerical value with '0' characters up to a number of digits left and right of the decimal point. If the + * number of digits right of the decimal point is zero, no decimal point will be rendered in the result. The sum of + * digits requested must be greater than zero. Digits will appear left of the decimal point if required to represent + * the integer part of the {@code value}'s magnitude. + * + * @param digitsLeftOfDecimal + * a non-negative number; if zero, + * @param digitsRightOfDecimal + * a non-negative number; if zero, no decimal point will appear at all + * @param round + * whether or not the value shall be rounded to the number of decimals requested. If {@code false}, the + * value will be displayed in a truncated form + */ + public static String padPositiveValue(double value, int digitsLeftOfDecimal, int digitsRightOfDecimal, boolean round) { + assert value >= 0; + assert digitsLeftOfDecimal >= 0; + assert digitsRightOfDecimal >= 0; + final StringBuilder sb = new StringBuilder(); + final double scalePow = Math.pow(10.0, digitsRightOfDecimal); + final double scaledValue = round?Math.round(value * scalePow):(value*scalePow); + double remainder = scaledValue; + if (digitsLeftOfDecimal==0 && scaledValue>=scalePow) { + sb.append(Math.round(remainder/scalePow)); + remainder = remainder%scalePow; + } + for (int i=digitsLeftOfDecimal+digitsRightOfDecimal; i>0; i--) { + if (i==digitsRightOfDecimal) { + sb.append('.'); + } + final double pow = Math.pow(10.0, i-1); + sb.append((int) (remainder/pow)); + remainder = remainder % pow; + } + return sb.toString(); + } } \ No newline at end of file From ca360006c5e74bf8a26a561cfd0d2f45cb9d7ede Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 17:33:41 +0100 Subject: [PATCH 023/214] WindFinder arrows appear on RaceMap Change-Id: I1775c71cf7c1820537c4b1632991eb4c30751725 --- .../impl/WindFinderPollerTest.java | 2 +- .../META-INF/MANIFEST.MF | 1 + .../domain/windfinderadapter/Spot.java | 10 ++++ .../WindFinderTrackerFactory.java | 16 +++++ .../windfinderadapter/impl/Activator.java | 3 +- .../windfinderadapter/impl/SpotImpl.java | 26 +++++++++ ...java => WindFinderTrackerFactoryImpl.java} | 27 +++++++-- .../impl/WindFinderWindTracker.java | 13 +++-- .../META-INF/MANIFEST.MF | 3 +- .../resources/SailingAnalyticsNotes.html | 7 +++ .../sailing/gwt/ui/client/SailingService.java | 2 + .../gwt/ui/client/SailingServiceAsync.java | 12 +++- .../sailing/gwt/ui/client/StringMessages.java | 1 + .../gwt/ui/client/StringMessages.properties | 3 +- .../ui/client/StringMessages_de.properties | 3 +- .../gwt/ui/client/shared/racemap/RaceMap.java | 28 ++++++++- .../gwt/ui/server/SailingServiceImpl.java | 58 +++++++++++++------ 17 files changed, 181 insertions(+), 34 deletions(-) create mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java rename java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/{WindFinderTrackerFactory.java => WindFinderTrackerFactoryImpl.java} (81%) diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java index c5c77b06628..6785021b74d 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java @@ -29,7 +29,7 @@ public class WindFinderPollerTest { @Test public void testPolling() throws Exception { final Object monitor = new Object(); - WindFinderTrackerFactory factory = new WindFinderTrackerFactory(); + WindFinderTrackerFactoryImpl factory = new WindFinderTrackerFactoryImpl(); factory.addReviewedSpotCollection(new ReviewedSpotsCollectionImpl("schilksee")); final List> wind = new ArrayList<>(); final RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class); diff --git a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF index 16088fed7c6..b3911f41fef 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF @@ -14,3 +14,4 @@ Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sap.sse.common, com.sap.sailing.domain, com.sap.sse +Export-Package: com.sap.sailing.domain.windfinderadapter diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java index f3601e27294..0afd20f070a 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -8,6 +8,7 @@ import java.text.ParseException; import com.sap.sailing.domain.common.Positioned; import com.sap.sailing.domain.common.Wind; import com.sap.sse.common.NamedWithID; +import com.sap.sse.common.TimePoint; /** * A measurement spot for which a report and / or a forecast may exist. Equality and @@ -48,6 +49,15 @@ public interface Spot extends NamedWithID, Positioned { * @return the URL of a web page showing general wind statistics for this spot */ URL getStatisticsUrl() throws MalformedURLException; + + /** + * For this spot provides a {@link URL} that a web UI can use to link + * to the WindFinder web site with the content most appropriate given the time point. + * This could be the report page if the time is about now; the forecast page if the time point + * is up to ten days in the future, or the statistics page if the time point is out of any + * of the scopes above. + */ + URL getCurrentlyMostAppropriateUrl(TimePoint timePoint) throws MalformedURLException; Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java new file mode 100755 index 00000000000..3867da1a3c8 --- /dev/null +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.windfinderadapter; + +import java.io.IOException; +import java.net.MalformedURLException; + +import org.json.simple.parser.ParseException; + +import com.sap.sailing.domain.tracking.WindTrackerFactory; + +public interface WindFinderTrackerFactory extends WindTrackerFactory { + + Iterable getReviewedSpotsCollections(); + + Spot getSpotById(String spotId) throws MalformedURLException, IOException, ParseException; + +} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java index 03235f1d0bf..b1cfaa7726a 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -9,6 +9,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import com.sap.sailing.domain.tracking.WindTrackerFactory; +import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); @@ -34,7 +35,7 @@ public class Activator implements BundleActivator { public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; logger.info("Creating ExpeditionTrackerFactory"); - final WindFinderTrackerFactory windfinderTrackerFactory = new WindFinderTrackerFactory(); + final WindFinderTrackerFactoryImpl windfinderTrackerFactory = new WindFinderTrackerFactoryImpl(); registrations.add(context.registerService(WindFinderTrackerFactory.class, windfinderTrackerFactory, /* properties */null)); registrations.add(context.registerService(WindTrackerFactory.class, windfinderTrackerFactory, /* properties */null)); } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index 35493ef96cd..f89b6ebb70a 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -17,7 +17,10 @@ import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.NamedImpl; public class SpotImpl extends NamedImpl implements Spot { @@ -26,6 +29,9 @@ public class SpotImpl extends NamedImpl implements Spot { private static final String BASE_REPORT_URL = BASE_URL + "/report"; private static final String BASE_FORECAST_URL = BASE_URL + "/forecast"; private static final String BASE_STATISTICS_URL = BASE_URL + "/windstatistics"; + + private static final Duration FORECAST_DURATION = Duration.ONE_DAY.times(9); + private static final Duration REPORT_LOOKBACK = Duration.ONE_DAY.times(7); private final String id; private final String keyword; @@ -104,6 +110,26 @@ public class SpotImpl extends NamedImpl implements Spot { return new URL(BASE_STATISTICS_URL+"/"+getKeyword()); } + @Override + public URL getCurrentlyMostAppropriateUrl(TimePoint timePoint) throws MalformedURLException { + final URL result; + final TimePoint now = MillisecondsTimePoint.now(); + if (timePoint.compareTo(now.plus(Duration.ONE_MINUTE))<=0) { // up to one minute into the future + if (timePoint.compareTo(now.minus(REPORT_LOOKBACK))>=0) { // and up to seven days back + result = getReportUrl(); // is covered by the report page; + } else { // we're looking further into the past; so it's the statistics page + result = getStatisticsUrl(); + } + } else { // we're looking into the future + if (timePoint.compareTo(now.plus(FORECAST_DURATION))<=0) { // and it's still covered by the forecast + result = getForecastUrl(); + } else { // otherwise we again resort to the stats page + result = getStatisticsUrl(); + } + } + return result; + } + @Override public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException { final InputStream response = (InputStream) getMeasurementsUrl().getContent(); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java similarity index 81% rename from java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java rename to java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java index b06658d0ad3..8543f77e5a8 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactory.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.windfinderadapter.impl; +import java.io.IOException; +import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -8,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.json.simple.parser.ParseException; import org.osgi.util.tracker.ServiceTracker; import com.sap.sailing.domain.base.RaceDefinition; @@ -15,12 +18,13 @@ import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.WindTracker; -import com.sap.sailing.domain.tracking.WindTrackerFactory; import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; import com.sap.sse.common.Util; import com.sap.sse.util.ServiceTrackerFactory; -public class WindFinderTrackerFactory implements WindTrackerFactory { +public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { private final Map windTrackerPerRace; /** @@ -32,7 +36,7 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { private Set reviewedSpotsCollections; private final ServiceTracker reviewedSpotsCollectionIdProvider; - public WindFinderTrackerFactory() { + public WindFinderTrackerFactoryImpl() { this.windTrackerPerRace = new HashMap<>(); this.reviewedSpotsCollections = Collections.synchronizedSet(new HashSet<>()); if (Activator.getContext() != null) { @@ -80,7 +84,8 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { * * @return a non-live set of spots collections known by this factory at this point in time */ - Iterable getReviewedSpotsCollections() { + @Override + public Iterable getReviewedSpotsCollections() { final List result = new ArrayList<>(); result.addAll(reviewedSpotsCollections); final WindFinderReviewedSpotsCollectionIdProvider provider; @@ -89,6 +94,20 @@ public class WindFinderTrackerFactory implements WindTrackerFactory { } return result; } + + @Override + public Spot getSpotById(String spotId) throws MalformedURLException, IOException, ParseException { + Spot result = null; + for (final ReviewedSpotsCollection coll : getReviewedSpotsCollections()) { + for (final Spot spot : coll.getSpots()) { + if (Util.equalsWithNull(spot.getId(), spotId)) { + result = spot; + break; + } + } + } + return result; + } public void addReviewedSpotCollection(ReviewedSpotsCollection collection) { reviewedSpotsCollections.add(collection); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index 7735cef1e2b..dfb2e7b6211 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -31,7 +31,7 @@ import com.sap.sse.util.ThreadPoolUtil; *

* * {@link #stop() Stopping} this tracker will cancel the task that polls for regular updates and will tell the - * {@link WindFinderTrackerFactory} that this tracker has been stopped. The tracker will therefore be released by the + * {@link WindFinderTrackerFactoryImpl} that this tracker has been stopped. The tracker will therefore be released by the * factory and will no longer be returned as the tracker responsible for retrieving WindFinder data for the particular * race that this tracker was bound to. * @@ -43,7 +43,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private static final Duration POLL_EVERY = Duration.ONE_MINUTE; private final DynamicTrackedRace trackedRace; - private final WindFinderTrackerFactory factory; + private final WindFinderTrackerFactoryImpl factory; private final ScheduledFuture poller; @@ -54,7 +54,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { */ private final Iterable allSpotCollections; - public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactory factory) { + public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory) { this.trackedRace = trackedRace; this.factory = factory; this.allSpotCollections = factory.getReviewedSpotsCollections(); @@ -72,7 +72,12 @@ public class WindFinderWindTracker implements WindTracker, Runnable { for (final Spot usefulSpot : usefulSpots) { final Wind wind = usefulSpot.getLatestMeasurement(); if (wind != null) { - trackedRace.recordWind(wind, new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId())); + final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId()); + final Wind existingFix = trackedRace.getOrCreateWindTrack(windSource).getFirstRawFixAtOrAfter(wind.getTimePoint()); + // don't add the same fix twice + if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) { + trackedRace.recordWind(wind, windSource); + } } } } catch (Exception e) { diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index b3046564b6c..e737fe54d3b 100644 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -56,7 +56,8 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.competitorimport, com.sap.sailing.expeditionconnector, com.sap.sailing.expeditionconnector.common, - org.mp4parser.isoparser;bundle-version="1.9.31" + org.mp4parser.isoparser;bundle-version="1.9.31", + com.sap.sailing.domain.windfinderadapter Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator Bundle-ActivationPolicy: lazy Import-Package: javax.servlet;version="3.1.0", diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html index a931629d8fb..9b149b775d9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html @@ -4,6 +4,13 @@

What's New - SAP Sailing Analytics

+
January 2018
+
    +
  • Coordinate display (lat/lng), e.g., in pop-ups for marks and wind sensors + now show the more common DD°MM.MMM' format. +
  • +
+
November 2017
  • The language preference set in the user profile (if any), now also affects the website's language. Formerly, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 35d860f6bc7..dcd8ea67d52 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -710,4 +710,6 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS throws NotFoundException, CompetitorRegistrationOnRaceLogDisabledException; List getRaceDisplayNamesFromLeaderboard(String leaderboardName, List raceColumnNames) throws NotFoundException; + + String getWindFinderLink(TimePoint forWhichTime, String spotId) throws Exception; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index e9e2f1ea440..a49428d9403 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -1,5 +1,6 @@ package com.sap.sailing.gwt.ui.client; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Date; @@ -926,5 +927,14 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void fillRaceLogsFromPairingListTemplate(final String leaderboardName, final int flightMultiplier, final Iterable selectedFlightNames,PairingListDTO pairingListDTO, AsyncCallback callback); - void getRaceDisplayNamesFromLeaderboard(final String leaderboardName,List raceColumnNames, AsyncCallback> callback); + void getRaceDisplayNamesFromLeaderboard(final String leaderboardName, List raceColumnNames, AsyncCallback> callback); + + /** + * For a given WindFinder spot ID provides a {@link URL} that a web UI can use to link + * to the WindFinder web site with the content most appropriate given the time point. + * This could be the report page if the time is about now; the forecast page if the time point + * is up to ten days in the future, or the statistics page if the time point is out of any + * of the scopes above. + */ + void getWindFinderLink(TimePoint forWhichTime, String spotId, AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index e7934835f46..c065a187bdb 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2032,4 +2032,5 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String enterTagsForTheVideo(); String enterIdOfWindfinderReviewedSpotCollection(); String enterTagsForTheImage(); + String unableToResolveWindFinderSpotId(String id, String message); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index f8533fac056..21febecfb49 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2020,4 +2020,5 @@ windFinderWindSourceTypeTooltip=Measured wind from one or more spots of www.wind windFinder=WindFinder enterTagsForTheVideo=Enter tags for the video enterIdOfWindfinderReviewedSpotCollection=Enter ID of a reviewed WindFinder spot collection, e.g., "schilksee" -enterTagsForTheImage=Enter tags for the image \ No newline at end of file +enterTagsForTheImage=Enter tags for the image +unableToResolveWindFinderSpotId=Unable to resolve WindFinder spot with ID {0}: {1} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index aa02fe6bd65..1f132088bd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2014,4 +2014,5 @@ windFinderWindSourceTypeTooltip=Gemessener Wind von einer oder mehreren Statione windFinder=WindFinder enterTagsForTheVideo=Tags zum Video erfassen enterIdOfWindfinderReviewedSpotCollection=ID einer geprüften WindFinder Messstellen-Sammlung eingeben, z.B. "schilksee" -enterTagsForTheImage=Tags zum Bild erfassen \ No newline at end of file +enterTagsForTheImage=Tags zum Bild erfassen +unableToResolveWindFinderSpotId=WindFinder-Station mit Kennung {0} wurde nicht gefunden: {1} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 357e3fd0394..e9cd83ffd46 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -63,6 +63,7 @@ import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Image; @@ -135,6 +136,7 @@ import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; import com.sap.sse.common.filter.Filter; import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.RGBColor; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; @@ -859,6 +861,7 @@ public class RaceMap extends AbstractCompositeComponent impleme // draw the wind into the map, get the combined wind List windSourceTypeNames = new ArrayList(); windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); + windSourceTypeNames.add(WindSourceType.WINDFINDER.name()); windSourceTypeNames.add(WindSourceType.COMBINED.name()); GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, race, newTime, 1000L, 1, windSourceTypeNames, /* onlyUpToNewestEvent==false means get us any data we can get by a best effort */ false); @@ -2171,6 +2174,7 @@ public class RaceMap extends AbstractCompositeComponent impleme VerticalPanel vPanel = new VerticalPanel(); vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), markDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), markDTO.position.getAsSignedDecimalDegrees())); return vPanel; } @@ -2198,18 +2202,38 @@ public class RaceMap extends AbstractCompositeComponent impleme return vPanel; } - private Widget getInfoWindowContent(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { + private Widget getInfoWindowContent(final WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); NumberFormat numberFormat = NumberFormat.getFormat("0.0"); - VerticalPanel vPanel = new VerticalPanel(); + final VerticalPanel vPanel = new VerticalPanel(); vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); + final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsSignedDecimalDegrees())); + if (windSource.getType() == WindSourceType.WINDFINDER) { + sailingService.getWindFinderLink(timePoint, windSource.getId().toString(), new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.unableToResolveWindFinderSpotId(windSource.getId().toString(), caught.getMessage()), /* silentMode */ true); + } + + @Override + public void onSuccess(String result) { + vPanel.add(createWindFinderButtonLink(windSource.getId().toString(), result)); + } + }); + } return vPanel; } + private Widget createWindFinderButtonLink(String spotId, String url) { + // TODO bug1301 place WindFinder button icon on anchor + return new Anchor(stringMessages.windFinder(), url); + } + private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType lastFix) { final VerticalPanel vPanel = new VerticalPanel(); vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index f14e32c8fe9..13449d21b8a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -311,6 +311,8 @@ import com.sap.sailing.domain.tractracadapter.TracTracAdapter; import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; +import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; import com.sap.sailing.expeditionconnector.ExpeditionDeviceConfiguration; import com.sap.sailing.expeditionconnector.ExpeditionSensorDeviceIdentifier; import com.sap.sailing.expeditionconnector.ExpeditionTrackerFactory; @@ -536,6 +538,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private final ServiceTracker scoreCorrectionProviderServiceTracker; private final ServiceTracker competitorProviderServiceTracker; + + private final ServiceTracker windFinderTrackerFactoryServiceTracker; private final MongoObjectFactory mongoObjectFactory; @@ -596,6 +600,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } quickRanksLiveCache = new QuickRanksLiveCache(this); racingEventServiceTracker = ServiceTrackerFactory.createAndOpen(context, RacingEventService.class); + windFinderTrackerFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, WindFinderTrackerFactory.class); replicationServiceTracker = ServiceTrackerFactory.createAndOpen(context, ReplicationService.class); resultUrlRegistryServiceTracker = ServiceTrackerFactory.createAndOpen(context, ResultUrlRegistry.class); swissTimingAdapterTracker = ServiceTrackerFactory.createAndOpen(context, SwissTimingAdapterFactory.class); @@ -6909,45 +6914,62 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } - public List getRaceDisplayNamesFromLeaderboard(String leaderboardName,List raceColumnNames) throws NotFoundException{ - Leaderboard leaderboard=this.getLeaderboardByName(leaderboardName); - List result=new ArrayList<>(); + public List getRaceDisplayNamesFromLeaderboard(String leaderboardName,List raceColumnNames) throws NotFoundException { + Leaderboard leaderboard = this.getLeaderboardByName(leaderboardName); + List result = new ArrayList<>(); for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - if(raceColumn.hasTrackedRaces()){ + if (raceColumn.hasTrackedRaces()) { if (raceColumnNames.contains(raceColumn.getName())) { for (Fleet fleet : raceColumn.getFleets()) { - if(raceColumn.getTrackedRace(fleet) != null && raceColumn.getTrackedRace(fleet).getRaceIdentifier()!=null){ + if(raceColumn.getTrackedRace(fleet) != null && raceColumn.getTrackedRace(fleet).getRaceIdentifier()!=null) { result.add(raceColumn.getTrackedRace(fleet).getRaceIdentifier().getRaceName()); } } } - }else{ + } else { break; } } - if(result.size()==raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets())){ + if (result.size()==raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets())) { return result; } result.clear(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()){ - for(Fleet fleet: raceColumn.getFleets()){ - NavigableSet set=raceColumn.getRaceLog(fleet).getUnrevokedEvents(); - for (RaceLogEvent raceLogEvent : set) { - if(raceLogEvent instanceof RaceLogDenoteForTrackingEvent){ - RaceLogDenoteForTrackingEvent denoteEvent = (RaceLogDenoteForTrackingEvent) raceLogEvent; - result.add(denoteEvent.getRaceName()); - break; - } + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet: raceColumn.getFleets()) { + NavigableSet set=raceColumn.getRaceLog(fleet).getUnrevokedEvents(); + for (RaceLogEvent raceLogEvent : set) { + if (raceLogEvent instanceof RaceLogDenoteForTrackingEvent) { + RaceLogDenoteForTrackingEvent denoteEvent = (RaceLogDenoteForTrackingEvent) raceLogEvent; + result.add(denoteEvent.getRaceName()); + break; } } + } } - if(result.size()==raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets())){ + if (result.size()==raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets())) { return result; } result.clear(); - for(int count=1;count<=raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets());count++){ + for (int count=1;count<=raceColumnNames.size()*Util.size(leaderboard.getRaceColumnByName(raceColumnNames.get(0)).getFleets());count++) { result.add("Race "+count); } return result; } + + @Override + public String getWindFinderLink(TimePoint forWhichTime, String spotId) throws MalformedURLException, IOException, org.json.simple.parser.ParseException { + final URL result; + final WindFinderTrackerFactory windFinderTrackerFactory = windFinderTrackerFactoryServiceTracker.getService(); + if (windFinderTrackerFactory != null) { + final Spot spot = windFinderTrackerFactory.getSpotById(spotId); + if (spot != null) { + result = spot.getCurrentlyMostAppropriateUrl(forWhichTime); + } else { + result = null; + } + } else { + result = null; + } + return result.toString(); + } } From a0f03b3bf3775a457eeb02e60248c606da769a50 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 18:16:00 +0100 Subject: [PATCH 024/214] integrate WindFinder link in info window; adjusted launch configs; avoid duplicate wind fixes Change-Id: I93bf2944efab280b8a900c1c66a5c418ba8060c8 --- .../domain/windfinderadapter/Spot.java | 9 +++++ .../windfinderadapter/impl/SpotImpl.java | 22 ++++++++++-- .../impl/WindFinderWindTracker.java | 22 ++++++++---- .../gwt/ui/client/shared/racemap/RaceMap.java | 34 +++++++++---------- .../SailingServer (No Proxy OSX).launch | 2 +- ...ailingServer (No Proxy, Cached MTB).launch | 2 +- ...ingServer (No Proxy, Jetty on 8889).launch | 2 +- ...o Proxy, Jetty on 8889, Cached MTB).launch | 2 +- ... Jetty on 8889, auto-replicate dev).launch | 2 +- ...oxy, Jetty on 8889, auto-replicate).launch | 2 +- ...ingServer (No Proxy, Jetty on 8890).launch | 2 +- ...ver (No Proxy, Remote Debug SAP VM).launch | 2 +- ...ailingServer (No Proxy, winddbTest).launch | 2 +- .../SailingServer (Proxy).launch | 2 +- ...ailingServer (Proxy, Jetty on 8889).launch | 2 +- ...Server (Proxy, Remote Debug SAP VM).launch | 2 +- ...Proxy, SwissTiming Live Simulation).launch | 2 +- .../SailingServer (Proxy, winddbTest).launch | 2 +- 18 files changed, 76 insertions(+), 39 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java index 0afd20f070a..a276e7ea03c 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java @@ -60,4 +60,13 @@ public interface Spot extends NamedWithID, Positioned { URL getCurrentlyMostAppropriateUrl(TimePoint timePoint) throws MalformedURLException; Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException; + + Iterable getAllMeasurements() throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException; + + /** + * @param timePoint + * if {@code null} then this call delivers the result of {@link #getAllMeasurements()} + * @return an always valid but possibly empty collection + */ + Iterable getAllMeasurementsAfter(TimePoint timePoint) throws MalformedURLException, IOException, ParseException, org.json.simple.parser.ParseException; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index f89b6ebb70a..860291fba84 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -132,8 +132,7 @@ public class SpotImpl extends NamedImpl implements Spot { @Override public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException { - final InputStream response = (InputStream) getMeasurementsUrl().getContent(); - final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(new InputStreamReader(response))); + final Iterable measurements = getAllMeasurements(); final Wind result; if (measurements != null && !Util.isEmpty(measurements)) { final List measurementsSortedByTimepoint = new ArrayList<>(); @@ -146,6 +145,25 @@ public class SpotImpl extends NamedImpl implements Spot { return result; } + @Override + public Iterable getAllMeasurementsAfter(TimePoint timePoint) throws MalformedURLException, IOException, ParseException, org.json.simple.parser.ParseException { + final List result = new ArrayList<>(); + for (final Wind measurement : getAllMeasurements()) { + if (timePoint == null || measurement.getTimePoint().after(timePoint)) { + result.add(measurement); + } + } + return result; + } + + @Override + public Iterable getAllMeasurements() + throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException { + final InputStream response = (InputStream) getMeasurementsUrl().getContent(); + final Iterable measurements = parser.parse(getPosition(), (JSONArray) new JSONParser().parse(new InputStreamReader(response))); + return measurements; + } + private URL getMeasurementsUrl() throws MalformedURLException { return new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+collection.getId()+"_"+getId()+".json"); } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index dfb2e7b6211..aac59e1740b 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -15,10 +15,12 @@ import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindTracker; import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; import com.sap.sailing.domain.windfinderadapter.Spot; import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.util.ThreadPoolUtil; @@ -47,6 +49,8 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private final ScheduledFuture poller; + private TimePoint timePointOfLastMeasurement; + /** * The set of all {@link ReviewedSpotsCollection}s delivered by the {@link #factory} when this tracker * was created. This is the basis for {@link #getUsefulSpots()} when evaluating, e.g., based on the @@ -70,13 +74,19 @@ public class WindFinderWindTracker implements WindTracker, Runnable { try { final Iterable usefulSpots = getUsefulSpots(); for (final Spot usefulSpot : usefulSpots) { - final Wind wind = usefulSpot.getLatestMeasurement(); - if (wind != null) { + final Iterable windFixes = usefulSpot.getAllMeasurementsAfter(timePointOfLastMeasurement); + if (!Util.isEmpty(windFixes)) { final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId()); - final Wind existingFix = trackedRace.getOrCreateWindTrack(windSource).getFirstRawFixAtOrAfter(wind.getTimePoint()); - // don't add the same fix twice - if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) { - trackedRace.recordWind(wind, windSource); + final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + for (final Wind wind : windFixes) { + final Wind existingFix = windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint()); + if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) { + // avoid duplicates by adding the same fix again + trackedRace.recordWind(wind, windSource); + if (timePointOfLastMeasurement == null || wind.getTimePoint().after(timePointOfLastMeasurement)) { + timePointOfLastMeasurement = wind.getTimePoint(); + } + } } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index e9cd83ffd46..8f1476b996d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2153,20 +2153,22 @@ public class RaceMap extends AbstractCompositeComponent impleme } private Widget createInfoWindowLabelAndValue(String labelName, String value) { - FlowPanel flowPanel = new FlowPanel(); + Label valueLabel = new Label(value); + valueLabel.setWordWrap(false); + valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); + return createInfoWindowLabelWithWidget(labelName, valueLabel); + } + + public Widget createInfoWindowLabelWithWidget(String labelName, Widget value) { + FlowPanel flowPanel = new FlowPanel(); Label label = new Label(labelName + ":"); label.setWordWrap(false); label.getElement().getStyle().setFloat(Style.Float.LEFT); label.getElement().getStyle().setPadding(3, Style.Unit.PX); label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); flowPanel.add(label); - - Label valueLabel = new Label(value); - valueLabel.setWordWrap(false); - valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); - flowPanel.add(valueLabel); - + flowPanel.add(value); return flowPanel; } @@ -2206,11 +2208,12 @@ public class RaceMap extends AbstractCompositeComponent impleme WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); NumberFormat numberFormat = NumberFormat.getFormat("0.0"); final VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); + final Anchor windSourceNameAnchor = new Anchor(WindSourceTypeFormatter.format(windSource, stringMessages)); + vPanel.add(createInfoWindowLabelWithWidget(stringMessages.windSource(), windSourceNameAnchor)); vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); - final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); + final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.asDate().toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsSignedDecimalDegrees())); if (windSource.getType() == WindSourceType.WINDFINDER) { @@ -2222,18 +2225,15 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void onSuccess(String result) { - vPanel.add(createWindFinderButtonLink(windSource.getId().toString(), result)); + // TODO bug1301 place WindFinder button icon on anchor + windSourceNameAnchor.setHref(result); + windSourceNameAnchor.setTarget("_blank"); } }); } return vPanel; } - private Widget createWindFinderButtonLink(String spotId, String url) { - // TODO bug1301 place WindFinder button icon on anchor - return new Anchor(stringMessages.windFinder(), url); - } - private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType lastFix) { final VerticalPanel vPanel = new VerticalPanel(); vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch index 347ccc9c0a2..78f45bbd469 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch @@ -34,5 +34,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch index 8b6711543ed..528877e8879 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch @@ -33,5 +33,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch index 73eaccf695b..c836c6a3610 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch index 4ad15da213b..6726bfee7f1 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch index caf29b6304b..9bf306492cd 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch index dd0241b2c75..640639b4e53 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch @@ -29,6 +29,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch index bbe59eca851..71d3577cc6a 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch index a3578a7ed6a..51e60de772c 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch @@ -27,5 +27,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch index 7cfe423142f..c9bd486f29e 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch @@ -33,5 +33,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy).launch b/java/com.sap.sailing.server/SailingServer (Proxy).launch index dd36f97dae2..98ff27d0f46 100644 --- a/java/com.sap.sailing.server/SailingServer (Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy).launch @@ -36,6 +36,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch index 5c308b7e960..7bbb211a005 100644 --- a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch index 098014ae706..aaed2528851 100644 --- a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch @@ -27,5 +27,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch index 73d478f2cff..fe45f03e85c 100644 --- a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch @@ -26,5 +26,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch index 67539b2c380..6a8fc3e3eeb 100644 --- a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch @@ -28,6 +28,6 @@ - + From 21b704ca81168e24221d0e58659bdcfc99014ef3 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 18:22:01 +0100 Subject: [PATCH 025/214] adjusted release notes for WindFinder integration Change-Id: I30b868d365d5c1160519ad69c8794fc0151d4ecc --- .../release_notes_admin.html | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index 8bdbf288a98..0d7de40b5c3 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -22,12 +22,28 @@

    Release Notes - Administration Console

    +

    January 2018

    +
      +
    • Events have now an additional "WindFinder" tab in their configuration dialog. + WindFinder can provide reviewed subsets of their measurement spots and + gives an ID for such a reviewed spot collection. Enter one or more of such + IDs into your event configuration if you'd like to include WindFinder + (https://www.windfinder.com) as an additional wind source for races that + you track. Note that when tracking with live wind, all wind finder spots + from all events available on your server will be considered for inclusion, + based largely on their distance from your event's venue. Note further that + a WindFinder-enabled event will show a "WindFinder" button on the event + web page through which users can navigate to the statistics, forecasts + and reports of the respective measurement spots.
    • +
    +

    December 2017

    • In the raceboard it is now possible to upload 360� videos in addition to the already supported video types. - 360� Videos must use MimeType mp4panorama, 2d mp4 videos must still use mimetype mp4, as in older releases. + 360° Videos must use MimeType mp4panorama, 2d mp4 videos must still use mimetype mp4, as in older releases. The dialog will try an auto-detection for the mimetype upon entering the link. - Additionally, for mp4 videos it is now possible to use the start time, written in the file, as an start time preset for the mediatrack, simplifying the video linking.
    • + Additionally, for mp4 videos it is now possible to use the start time, written in the file, as + an start time preset for the mediatrack, simplifying the video linking.
    • In Home it is now also possible to use 360� videos, for example as an highlight video
    • Various Fix importers can now properly report errors and other states
    • Pairing list generation is now supported. Add competitors to a regatta, then use the new From edd66ffca73b31fefb334cb763e0d078e7b7ddd8 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 5 Jan 2018 18:27:07 +0100 Subject: [PATCH 026/214] added a TODO Change-Id: Ifcc828b00bf7a3e080ebbacd4fe382ab4172b6df --- .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 8f1476b996d..212a31e4136 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2230,7 +2230,7 @@ public class RaceMap extends AbstractCompositeComponent impleme windSourceNameAnchor.setTarget("_blank"); } }); - } + } // TODO bug1301 else, or in any case, register an event listener on the Anchor that opens the wind chart return vPanel; } From 33d5146b65f76d0ff89c645bfe48c2b04d5d9669 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 6 Jan 2018 09:18:48 +0100 Subject: [PATCH 027/214] fix test case where mocked TrackedRace doesn't deliver wind tracks --- .../domain/windfinderadapter/impl/WindFinderWindTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index aac59e1740b..dc891aba1ce 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -79,7 +79,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId()); final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); for (final Wind wind : windFixes) { - final Wind existingFix = windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint()); + final Wind existingFix = windTrack==null?null/*test scenario*/:windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint()); if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) { // avoid duplicates by adding the same fix again trackedRace.recordWind(wind, windSource); From dcf2e5dbfb60f86328a8379401feab1cf9baf8d6 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 8 Jan 2018 11:25:02 +0100 Subject: [PATCH 028/214] added field for all (inferred) WindFinder sources to Event DTOs Change-Id: I0d8dcb8e6f08d6f273e38d0b7189f6d0de918fcb --- .../com/sap/sailing/domain/base/Event.java | 11 +++++++ .../sailing/domain/base/impl/EventImpl.java | 22 +++++++++++++ .../event/GetEventViewAction.java | 1 + .../communication/eventview/EventViewDTO.java | 29 +++++++++++++++++ .../gwt/ui/adminconsole/EventEditDialog.java | 2 +- .../ui/adminconsole/EventListComposite.java | 2 +- .../gwt/ui/server/SailingServiceImpl.java | 5 +-- .../sap/sailing/gwt/ui/shared/EventDTO.java | 32 +++++++++++++++++-- 8 files changed, 98 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java index b7da4927637..1fa1d107943 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java @@ -2,6 +2,8 @@ package com.sap.sailing.domain.base; import java.util.UUID; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; /** @@ -48,6 +50,15 @@ public interface Event extends EventBase { * and this getter returns the IDs last set. */ Iterable getWindFinderReviewedSpotsCollectionIds(); + + /** + * In addition to the wind finder spot collections specified by this event explicitly (see + * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spot collection IDs based + * on the tracked races reachable from this event's associated leaderboard groups and their wind sources. The + * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be + * collected and returned. + */ + Iterable getAllFinderReviewedSpotsCollectionIdsUsedByEvent(); /** * Set the IDs of the reviewed WindFinder spot collections to consider during this event. diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java index df08dad6ac2..336544ba3b0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java @@ -3,6 +3,8 @@ package com.sap.sailing.domain.base.impl; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -10,7 +12,11 @@ import java.util.concurrent.ConcurrentMap; import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -71,6 +77,22 @@ public class EventImpl extends EventBaseImpl implements Event { return Collections.unmodifiableSet(windFinderReviewedSpotsCollectionIds.keySet()); } + @Override + public Iterable getAllFinderReviewedSpotsCollectionIdsUsedByEvent() { + final Set result = new HashSet<>(); + Util.addAll(getWindFinderReviewedSpotsCollectionIds(), result); + for (final LeaderboardGroup leaderboardGroup : getLeaderboardGroups()) { + for (final Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + for (final TrackedRace trackedRace : leaderboard.getTrackedRaces()) { + for (final WindSource windTrackerWindSource : trackedRace.getWindSources(WindSourceType.WINDFINDER)) { + result.add(windTrackerWindSource.getId().toString()); + } + } + } + } + return result; + } + @Override public void setWindFinderReviewedSpotsCollection(Iterable reviewedSpotsCollectionIds) { windFinderReviewedSpotsCollectionIds.clear(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java index 921ce88f966..fb97d527461 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java @@ -62,6 +62,7 @@ public class GetEventViewAction implements SailingAction, IsClient dto.setOfficialWebsiteURL(event.getOfficialWebsiteURL() == null ? null : event.getOfficialWebsiteURL().toString()); URL sailorsInfoWebsiteURL = event.getSailorsInfoWebsiteURLOrFallback(context.getClientLocale()); dto.setSailorsInfoWebsiteURL(sailorsInfoWebsiteURL == null ? null : sailorsInfoWebsiteURL.toString()); + dto.setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(event.getAllFinderReviewedSpotsCollectionIdsUsedByEvent()); dto.setHasMedia(HomeServiceUtil.hasMedia(event)); dto.setState(HomeServiceUtil.calculateEventState(event)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java index 20ee063f1a4..1eedc515667 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java @@ -2,13 +2,17 @@ package com.sap.sailing.gwt.home.communication.eventview; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.TreeSet; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.dto.EventType; import com.sap.sailing.gwt.home.communication.event.EventMetadataDTO; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.event.HasLogo; +import com.sap.sse.common.Util; import com.sap.sse.gwt.client.media.ImageDTO; import com.sap.sse.gwt.dispatch.shared.commands.Result; @@ -24,6 +28,7 @@ public class EventViewDTO extends EventMetadataDTO implements Result, HasLogo { private String officialWebsiteURL; private String sailorsInfoWebsiteURL; private String description; + private List allWindFinderReviewedSpotsCollectionIdsUsedByEvent; public EventType getType() { return type; @@ -110,6 +115,30 @@ public class EventViewDTO extends EventMetadataDTO implements Result, HasLogo { this.sailorsInfoWebsiteURL = sailorsInfoWebsiteURL; } + /** + * In addition to the wind finder spot collections specified by this event explicitly (see + * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spot collection IDs based + * on the tracked races reachable from this event's associated leaderboard groups and their wind sources. The + * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be + * collected and returned. + */ + public Iterable getAllWindFinderReviewedSpotsCollectionIdsUsedByEvent() { + final Iterable result; + if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent == null) { + result = Collections.emptySet(); + } else { + result = allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + } + return result; + } + + public void setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(Iterable allWindFinderReviewedSpotsCollectionIdsUsedByEvent) { + this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent = new ArrayList<>(); + if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent != null) { + Util.addAll(allWindFinderReviewedSpotsCollectionIdsUsedByEvent, this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent); + } + } + public String getLocationAndVenueAndCountry() { String venue = getLocationAndVenue(); if(getVenueCountry() != null && !getVenueCountry().isEmpty()) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java index 78212b26c9b..593f8412492 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java @@ -37,7 +37,7 @@ public class EventEditDialog extends EventDialog { leaderboardGroupList.setValue(leaderboardGroupNames); imagesListComposite.fillImages(event.getImages()); videosListComposite.fillVideos(event.getVideos()); - windFinderSpotCollectionIdsComposite.setValue(event.getWindFinderReviewedSpotsCollection()); + windFinderSpotCollectionIdsComposite.setValue(event.getWindFinderReviewedSpotsCollectionIds()); externalLinksComposite.fillExternalLinks(event); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 6c5c7b75ede..c4982591cf0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -619,7 +619,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le updatedEvent.getOfficialWebsiteURL(), updatedEvent.getBaseURL(), updatedEvent.getSailorsInfoWebsiteURLs(), updatedEvent.getImages(), - updatedEvent.getVideos(), updatedEvent.getWindFinderReviewedSpotsCollection(), new AsyncCallback() { + updatedEvent.getVideos(), updatedEvent.getWindFinderReviewedSpotsCollectionIds(), new AsyncCallback() { @Override public void onFailure(Throwable t) { errorReporter.reportError("Error trying to update sailing event" + oldEvent.getName() + ": " + t.getMessage()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 13449d21b8a..f2fecdc4c22 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3919,9 +3919,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S eventDTO.venue.getCourseAreas().add(courseAreaDTO); } for (LeaderboardGroup lg : event.getLeaderboardGroups()) { - eventDTO.addLeaderboardGroup(convertToLeaderboardGroupDTO(lg, /* withGeoLocationData */false, withStatisticalData)); + eventDTO.addLeaderboardGroup(convertToLeaderboardGroupDTO(lg, /* withGeoLocationData */ false, withStatisticalData)); } eventDTO.setWindFinderReviewedSpotsCollection(event.getWindFinderReviewedSpotsCollectionIds()); + eventDTO.setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(event.getAllFinderReviewedSpotsCollectionIdsUsedByEvent()); return eventDTO; } @@ -4522,7 +4523,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S updateEvent(newEvent.id, newEvent.getName(), description, newEvent.startDate, newEvent.endDate, newEvent.venue, newEvent.isPublic, eventLeaderboardGroupUUIDs, newEvent.getOfficialWebsiteURL(), newEvent.getBaseURL(), newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos(), - newEvent.getWindFinderReviewedSpotsCollection()); + newEvent.getWindFinderReviewedSpotsCollectionIds()); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java index cf11b44a3c5..ba22aeeb7a5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java @@ -6,6 +6,8 @@ import java.util.Date; import java.util.List; import java.util.UUID; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; import com.sap.sse.common.Util; @@ -17,6 +19,7 @@ public class EventDTO extends EventBaseDTO { private List leaderboardGroups; // keeps the more specific type accessible in a type-safe way private List windFinderReviewedSpotsCollectionIds; + private List allWindFinderReviewedSpotsCollectionIdsUsedByEvent; public EventDTO() { this(new ArrayList()); @@ -92,7 +95,7 @@ public class EventDTO extends EventBaseDTO { * * @return always a valid {@link Iterable} which may, though, be empty */ - public Iterable getWindFinderReviewedSpotsCollection() { + public Iterable getWindFinderReviewedSpotsCollectionIds() { final Iterable result; if (windFinderReviewedSpotsCollectionIds == null) { result = Collections.emptySet(); @@ -109,7 +112,32 @@ public class EventDTO extends EventBaseDTO { */ public void setWindFinderReviewedSpotsCollection(Iterable reviewedSpotsCollectionId) { windFinderReviewedSpotsCollectionIds = new ArrayList<>(); - Util.addAll(reviewedSpotsCollectionId, windFinderReviewedSpotsCollectionIds); + if (reviewedSpotsCollectionId != null) { + Util.addAll(reviewedSpotsCollectionId, windFinderReviewedSpotsCollectionIds); + } } + /** + * In addition to the wind finder spot collections specified by this event explicitly (see + * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spot collection IDs based + * on the tracked races reachable from this event's associated leaderboard groups and their wind sources. The + * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be + * collected and returned. + */ + public Iterable getAllWindFinderReviewedSpotsCollectionIdsUsedByEvent() { + final Iterable result; + if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent == null) { + result = Collections.emptySet(); + } else { + result = allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + } + return result; + } + + public void setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(Iterable allWindFinderReviewedSpotsCollectionIdsUsedByEvent) { + this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent = new ArrayList<>(); + if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent != null) { + Util.addAll(allWindFinderReviewedSpotsCollectionIdsUsedByEvent, this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent); + } + } } From 333c95acf0e52bd9539ff6bf74db1c485892c52d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 8 Jan 2018 18:48:52 +0100 Subject: [PATCH 029/214] added caching for WindFinder spot structure Change-Id: If778ada562ecf81cab4e8e35de9853e196876c78 --- .../META-INF/MANIFEST.MF | 3 +- .../domain/common/windfinder/SpotBase.java} | 27 +--- .../domain/common/windfinder/SpotDTO.java | 118 ++++++++++++++++++ .../impl/WindFinderReportParserTest.java | 11 +- .../META-INF/MANIFEST.MF | 1 - .../WindFinderTrackerFactory.java | 16 --- .../windfinderadapter/impl/Activator.java | 2 +- .../impl/ReviewedSpotsCollectionImpl.java | 46 ++++++- .../windfinderadapter/impl/SpotImpl.java | 109 +--------------- .../impl/WindFinderReportParser.java | 4 +- .../impl/WindFinderTrackerFactoryImpl.java | 98 +++++++++++---- .../impl/WindFinderWindTracker.java | 13 +- .../META-INF/MANIFEST.MF | 1 + .../com/sap/sailing/domain/base/Event.java | 22 ++-- .../sailing/domain/base/impl/EventImpl.java | 3 +- .../windfinder}/ReviewedSpotsCollection.java | 11 +- .../sap/sailing/domain/windfinder/Spot.java | 29 +++++ .../windfinder/WindFinderTrackerFactory.java | 38 ++++++ .../SpotDTO_CustomFieldSerializer.java | 54 ++++++++ .../communication/SailingDispatchContext.java | 4 + .../event/GetEventViewAction.java | 27 +++- .../communication/eventview/EventViewDTO.java | 25 ++-- .../server/SailingDispatchContextImpl.java | 13 +- .../servlets/SailingDispatchServlet.java | 4 + .../sailing/gwt/ui/client/SailingService.java | 3 +- .../gwt/ui/client/SailingServiceAsync.java | 3 +- .../gwt/ui/client/shared/racemap/RaceMap.java | 18 ++- .../shared/racemap/RaceMapImageManager.java | 4 + .../shared/racemap/RaceMapResources.java | 3 + .../gwt/ui/server/SailingServiceImpl.java | 38 ++++-- .../sap/sailing/gwt/ui/shared/EventDTO.java | 19 +-- .../gwt/ui/client/images/windfinder_small.png | Bin 0 -> 23160 bytes 32 files changed, 528 insertions(+), 239 deletions(-) rename java/{com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java => com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotBase.java} (56%) create mode 100755 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotDTO.java delete mode 100755 java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java rename java/{com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter => com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder}/ReviewedSpotsCollection.java (56%) create mode 100755 java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java create mode 100755 java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java create mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/windfinder/SpotDTO_CustomFieldSerializer.java create mode 100755 java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder_small.png diff --git a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF index f90d64762b1..aa519f62ff6 100644 --- a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF @@ -24,7 +24,8 @@ Export-Package: com.sap.sailing.domain.common, com.sap.sailing.domain.common.sensordata, com.sap.sailing.domain.common.trackfiles, com.sap.sailing.domain.common.tracking, - com.sap.sailing.domain.common.tracking.impl + com.sap.sailing.domain.common.tracking.impl, + com.sap.sailing.domain.common.windfinder Require-Bundle: com.sap.sse.common, com.sap.sse.security.common, com.sap.sse.datamining.annotations;bundle-version="1.0.0" diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotBase.java similarity index 56% rename from java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotBase.java index a276e7ea03c..54aa1b46e64 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/Spot.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotBase.java @@ -1,12 +1,8 @@ -package com.sap.sailing.domain.windfinderadapter; +package com.sap.sailing.domain.common.windfinder; -import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; -import java.text.ParseException; import com.sap.sailing.domain.common.Positioned; -import com.sap.sailing.domain.common.Wind; import com.sap.sse.common.NamedWithID; import com.sap.sse.common.TimePoint; @@ -17,7 +13,7 @@ import com.sap.sse.common.TimePoint; * @author Axel Uhl (D043530) * */ -public interface Spot extends NamedWithID, Positioned { +public interface SpotBase extends NamedWithID, Positioned { /** * A spot's ID is always a {@link String} */ @@ -38,17 +34,17 @@ public interface Spot extends NamedWithID, Positioned { /** * @return the URL of a web page showing a current weather report for this spot */ - URL getReportUrl() throws MalformedURLException; + String getReportUrl(); /** * @return the URL of a web page showing a weather forecast for this spot */ - URL getForecastUrl() throws MalformedURLException; + String getForecastUrl(); /** * @return the URL of a web page showing general wind statistics for this spot */ - URL getStatisticsUrl() throws MalformedURLException; + String getStatisticsUrl(); /** * For this spot provides a {@link URL} that a web UI can use to link @@ -57,16 +53,5 @@ public interface Spot extends NamedWithID, Positioned { * is up to ten days in the future, or the statistics page if the time point is out of any * of the scopes above. */ - URL getCurrentlyMostAppropriateUrl(TimePoint timePoint) throws MalformedURLException; - - Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException; - - Iterable getAllMeasurements() throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException; - - /** - * @param timePoint - * if {@code null} then this call delivers the result of {@link #getAllMeasurements()} - * @return an always valid but possibly empty collection - */ - Iterable getAllMeasurementsAfter(TimePoint timePoint) throws MalformedURLException, IOException, ParseException, org.json.simple.parser.ParseException; + String getCurrentlyMostAppropriateUrl(TimePoint timePoint); } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotDTO.java new file mode 100755 index 00000000000..d3474074fa4 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/windfinder/SpotDTO.java @@ -0,0 +1,118 @@ +package com.sap.sailing.domain.common.windfinder; + +import com.sap.sailing.domain.common.Position; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.common.impl.NamedImpl; + +public class SpotDTO extends NamedImpl implements SpotBase { + private static final long serialVersionUID = 1642900468710612984L; + private static final String BASE_URL = "https://www.windfinder.com"; + private static final String BASE_REPORT_URL = BASE_URL + "/report"; + private static final String BASE_FORECAST_URL = BASE_URL + "/forecast"; + private static final String BASE_STATISTICS_URL = BASE_URL + "/windstatistics"; + + private static final Duration FORECAST_DURATION = Duration.ONE_DAY.times(9); + private static final Duration REPORT_LOOKBACK = Duration.ONE_DAY.times(7); + + private final String id; + private final String keyword; + private final String englishCountryName; + private final Position position; + + public SpotDTO(String name, String id, String keyword, String englishCountryName, Position position) { + super(name); + this.id = id; + this.keyword = keyword; + this.englishCountryName = englishCountryName; + this.position = position; + } + + /** + * Copy constructor + */ + public SpotDTO(SpotBase spot) { + this(spot.getName(), spot.getId(), spot.getKeyword(), spot.getEnglishCountryName(), spot.getPosition()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SpotDTO other = (SpotDTO) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getKeyword() { + return keyword; + } + + @Override + public String getEnglishCountryName() { + return englishCountryName; + } + + @Override + public String getReportUrl() { + return BASE_REPORT_URL+"/"+getKeyword(); + } + + @Override + public String getForecastUrl() { + return BASE_FORECAST_URL+"/"+getKeyword(); + } + + @Override + public String getStatisticsUrl() { + return BASE_STATISTICS_URL+"/"+getKeyword(); + } + + @Override + public String getCurrentlyMostAppropriateUrl(TimePoint timePoint) { + final String result; + final TimePoint now = MillisecondsTimePoint.now(); + if (timePoint.compareTo(now.plus(Duration.ONE_MINUTE))<=0) { // up to one minute into the future + if (timePoint.compareTo(now.minus(REPORT_LOOKBACK))>=0) { // and up to seven days back + result = getReportUrl(); // is covered by the report page; + } else { // we're looking further into the past; so it's the statistics page + result = getStatisticsUrl(); + } + } else { // we're looking into the future + if (timePoint.compareTo(now.plus(FORECAST_DURATION))<=0) { // and it's still covered by the forecast + result = getForecastUrl(); + } else { // otherwise we again resort to the stats page + result = getStatisticsUrl(); + } + } + return result; + } +} diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java index 0a92cd18581..4144e563ba8 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -9,6 +9,7 @@ import java.net.MalformedURLException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ExecutionException; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -19,7 +20,7 @@ import org.junit.Test; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinder.Spot; import com.sap.sse.common.Util; public class WindFinderReportParserTest { @@ -70,13 +71,13 @@ public class WindFinderReportParserTest { } @Test - public void testTwoSpotsInSchilksee() throws MalformedURLException, IOException, ParseException { - assertEquals(2, Util.size(new ReviewedSpotsCollectionImpl("schilksee").getSpots())); + public void testTwoSpotsInSchilksee() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { + assertEquals(2, Util.size(new ReviewedSpotsCollectionImpl("schilksee").getSpots(/* cached */ false))); } @Test - public void testSpotsInSchilkseeAreThoseWithIds_10044N_And_de15() throws MalformedURLException, IOException, ParseException { - final Iterable spots = new ReviewedSpotsCollectionImpl("schilksee").getSpots(); + public void testSpotsInSchilkseeAreThoseWithIds_10044N_And_de15() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { + final Iterable spots = new ReviewedSpotsCollectionImpl("schilksee").getSpots(/* cached */ false); final Set spotIds = new HashSet<>(); Util.addAll(Util.map(spots, s->s.getId()), spotIds); assertEquals(new HashSet<>(Arrays.asList("10044N", "de15")), spotIds); diff --git a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF index b3911f41fef..16088fed7c6 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.windfinderadapter/META-INF/MANIFEST.MF @@ -14,4 +14,3 @@ Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sap.sse.common, com.sap.sailing.domain, com.sap.sse -Export-Package: com.sap.sailing.domain.windfinderadapter diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java deleted file mode 100755 index 3867da1a3c8..00000000000 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/WindFinderTrackerFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sap.sailing.domain.windfinderadapter; - -import java.io.IOException; -import java.net.MalformedURLException; - -import org.json.simple.parser.ParseException; - -import com.sap.sailing.domain.tracking.WindTrackerFactory; - -public interface WindFinderTrackerFactory extends WindTrackerFactory { - - Iterable getReviewedSpotsCollections(); - - Spot getSpotById(String spotId) throws MalformedURLException, IOException, ParseException; - -} diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java index b1cfaa7726a..678b8f342e3 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/Activator.java @@ -9,7 +9,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import com.sap.sailing.domain.tracking.WindTrackerFactory; -import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java index a8091cd1e99..f1699cb4ff7 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java @@ -5,13 +5,20 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.json.simple.JSONArray; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; -import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; -import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinder.Spot; +import com.sap.sse.util.ThreadPoolUtil; public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { /** @@ -24,8 +31,22 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { private final String id; private final WindFinderReportParser parser; + /** + * Initialized during construction time with a future that is passed to the default foreground executor + * (foreground because shortly a UI request may be depending on it). The task scheduled will create a + * map initialized with the result of calling {@link #loadSpots()}. + */ + private Future> spotsByIdCache; + public ReviewedSpotsCollectionImpl(String id) { this.id = id; + this.spotsByIdCache = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().schedule(()->{ + final ConcurrentMap result = new ConcurrentHashMap<>(); + for (final Spot spot : loadSpots()) { + result.put(spot.getId(), spot); + } + return result; + }, /* delay */ 0, TimeUnit.MILLISECONDS); this.parser = new WindFinderReportParser(); } @@ -60,10 +81,25 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { } @Override - public Iterable getSpots() throws MalformedURLException, IOException, ParseException { + public Iterable getSpots(boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { + final Iterable result; + if (cached) { + result = new HashSet<>(spotsByIdCache.get().values()); + } else { + result = loadSpots(); + for (final Spot spot : result) { + spotsByIdCache.get().put(spot.getId(), spot); + } + } + return result; + } + + private Iterable loadSpots() throws IOException, ParseException, MalformedURLException { + final Iterable result; JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(new InputStreamReader( - (InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent())); - return parser.parseSpots(spotsAsJson, this); + (InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent())); + result = parser.parseSpots(spotsAsJson, this); + return result; } @Override diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java index 860291fba84..5cbf86a8081 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java @@ -15,121 +15,24 @@ import org.json.simple.parser.JSONParser; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; -import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; -import com.sap.sailing.domain.windfinderadapter.Spot; -import com.sap.sse.common.Duration; +import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinder.Spot; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.impl.NamedImpl; -public class SpotImpl extends NamedImpl implements Spot { - private static final long serialVersionUID = 1642900468710612984L; - private static final String BASE_URL = "https://www.windfinder.com"; - private static final String BASE_REPORT_URL = BASE_URL + "/report"; - private static final String BASE_FORECAST_URL = BASE_URL + "/forecast"; - private static final String BASE_STATISTICS_URL = BASE_URL + "/windstatistics"; - - private static final Duration FORECAST_DURATION = Duration.ONE_DAY.times(9); - private static final Duration REPORT_LOOKBACK = Duration.ONE_DAY.times(7); +public class SpotImpl extends SpotDTO implements Spot { + private static final long serialVersionUID = -7793334775486020313L; - private final String id; - private final String keyword; - private final String englishCountryName; - private final Position position; private final WindFinderReportParser parser; private final ReviewedSpotsCollection collection; public SpotImpl(String name, String id, String keyword, String englishCountryName, Position position, WindFinderReportParser parser, ReviewedSpotsCollection collection) { - super(name); - this.id = id; - this.keyword = keyword; - this.englishCountryName = englishCountryName; - this.position = position; + super(name, id, keyword, englishCountryName, position); this.parser = parser; this.collection = collection; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SpotImpl other = (SpotImpl) obj; - if (id == null) { - if (other.id != null) - return false; - } else if (!id.equals(other.id)) - return false; - return true; - } - - @Override - public Position getPosition() { - return position; - } - - @Override - public String getId() { - return id; - } - - @Override - public String getKeyword() { - return keyword; - } - - @Override - public String getEnglishCountryName() { - return englishCountryName; - } - - @Override - public URL getReportUrl() throws MalformedURLException { - return new URL(BASE_REPORT_URL+"/"+getKeyword()); - } - - @Override - public URL getForecastUrl() throws MalformedURLException { - return new URL(BASE_FORECAST_URL+"/"+getKeyword()); - } - - @Override - public URL getStatisticsUrl() throws MalformedURLException { - return new URL(BASE_STATISTICS_URL+"/"+getKeyword()); - } - - @Override - public URL getCurrentlyMostAppropriateUrl(TimePoint timePoint) throws MalformedURLException { - final URL result; - final TimePoint now = MillisecondsTimePoint.now(); - if (timePoint.compareTo(now.plus(Duration.ONE_MINUTE))<=0) { // up to one minute into the future - if (timePoint.compareTo(now.minus(REPORT_LOOKBACK))>=0) { // and up to seven days back - result = getReportUrl(); // is covered by the report page; - } else { // we're looking further into the past; so it's the statistics page - result = getStatisticsUrl(); - } - } else { // we're looking into the future - if (timePoint.compareTo(now.plus(FORECAST_DURATION))<=0) { // and it's still covered by the forecast - result = getForecastUrl(); - } else { // otherwise we again resort to the stats page - result = getStatisticsUrl(); - } - } - return result; - } - @Override public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException { final Iterable measurements = getAllMeasurements(); diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index 080030c3601..79d3e423b44 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -15,8 +15,8 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.WindImpl; -import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; -import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinder.Spot; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java index 8543f77e5a8..828f5a943e0 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java @@ -2,13 +2,16 @@ package com.sap.sailing.domain.windfinderadapter.impl; import java.io.IOException; import java.net.MalformedURLException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.json.simple.parser.ParseException; import org.osgi.util.tracker.ServiceTracker; @@ -18,20 +21,27 @@ import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.WindTracker; -import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; -import com.sap.sailing.domain.windfinderadapter.Spot; -import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; +import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinder.Spot; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sse.common.Util; import com.sap.sse.util.ServiceTrackerFactory; +import com.sap.sse.util.ThreadPoolUtil; public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { private final Map windTrackerPerRace; /** - * Can hold reviewed spots collections. Starts out as {@code null}. If this is the case, an attempt - * will be made to retrieve spot collection IDs from {@link #reviewedSpotsCollectionIdProvider} if - * that is not {@code null} either, e.g., because we're running outside of an OSGi environment such - * as a test set-up. + * The initial filling of this cache is either accomplished immediately if no + * {@link #reviewedSpotsCollectionIdProvider} is available, or the provider is waited for in a background thread and + * then requested to deliver its reviewed spot collection IDs, and once those are received, the future is fulfilled. + */ + private final Future> reviewedSpotsCollectionsByIdCache; + + /** + * Can hold reviewed spots collections. Starts out valid but empty. Useful if we're running outside of an OSGi + * environment such as a test set-up. The collections provided here will be added to the set returned by + * {@link #getReviewedSpotsCollections(boolean)}. */ private Set reviewedSpotsCollections; private final ServiceTracker reviewedSpotsCollectionIdProvider; @@ -41,8 +51,25 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { this.reviewedSpotsCollections = Collections.synchronizedSet(new HashSet<>()); if (Activator.getContext() != null) { reviewedSpotsCollectionIdProvider = ServiceTrackerFactory.createAndOpen(Activator.getContext(), WindFinderReviewedSpotsCollectionIdProvider.class); + reviewedSpotsCollectionsByIdCache = ThreadPoolUtil.INSTANCE.createBackgroundTaskThreadPoolExecutor(1, getClass().getName()+" init").schedule( + ()->{ + final ConcurrentMap result = new ConcurrentHashMap<>(); + reviewedSpotsCollectionIdProvider.getService(); + for (final ReviewedSpotsCollection c : loadSpotCollectionsFromProvider()) { + result.put(c.getId(), c); + } + return result; + }, /*delay*/ 0, TimeUnit.MILLISECONDS); } else { reviewedSpotsCollectionIdProvider = null; + final ConcurrentMap cache = new ConcurrentHashMap<>(); + this.reviewedSpotsCollectionsByIdCache = new Future>() { + @Override public boolean cancel(boolean mayInterruptIfRunning) { return true; } + @Override public boolean isCancelled() { return false; } + @Override public boolean isDone() { return true; } + @Override public ConcurrentMap get() { return cache; } + @Override public ConcurrentMap get(long timeout, TimeUnit unit) { return cache; } + }; } } @@ -76,30 +103,47 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { } } - /** - * Obtains the reviewed spot collections that this factory knows about. This set is constructed from the collections - * provided explicitly using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and - * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained - * through {@link #reviewedSpotsCollectionIdProvider} if a corresponding service can be resolved. - * - * @return a non-live set of spots collections known by this factory at this point in time - */ @Override - public Iterable getReviewedSpotsCollections() { - final List result = new ArrayList<>(); - result.addAll(reviewedSpotsCollections); + public Iterable getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException { + final Set result = new HashSet<>(); + if (cached) { + result.addAll(reviewedSpotsCollectionsByIdCache.get().values()); + } else { + result.addAll(reviewedSpotsCollections); + Util.addAll(loadSpotCollectionsFromProvider(), result); + for (final ReviewedSpotsCollection c : result) { + reviewedSpotsCollectionsByIdCache.get().putIfAbsent(c.getId(), c); + } + } + return result; + } + + private Iterable loadSpotCollectionsFromProvider() throws InterruptedException, ExecutionException { + final Set result = new HashSet<>(); final WindFinderReviewedSpotsCollectionIdProvider provider; if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) { - Util.addAll(Util.map(provider.getWindFinderReviewedSpotsCollectionIds(), id->new ReviewedSpotsCollectionImpl(id)), result); + for (String id : provider.getWindFinderReviewedSpotsCollectionIds()) { + result.add(getReviewedSpotsCollectionById(id)); + } } return result; } @Override - public Spot getSpotById(String spotId) throws MalformedURLException, IOException, ParseException { + public ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId) throws InterruptedException, ExecutionException { + ReviewedSpotsCollection result = reviewedSpotsCollectionsByIdCache.get().get(spotsCollectionId); + if (result == null) { + result = new ReviewedSpotsCollectionImpl(spotsCollectionId); + reviewedSpotsCollectionsByIdCache.get().put(spotsCollectionId, result); + } + return result; + } + + @Override + public Spot getSpotById(String spotId, boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { Spot result = null; - for (final ReviewedSpotsCollection coll : getReviewedSpotsCollections()) { - for (final Spot spot : coll.getSpots()) { + for (final ReviewedSpotsCollection coll : getReviewedSpotsCollections(/* cached */ cached)) { + for (final Spot spot : coll.getSpots(/* cached */ cached)) { if (Util.equalsWithNull(spot.getId(), spotId)) { result = spot; break; @@ -109,11 +153,13 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { return result; } - public void addReviewedSpotCollection(ReviewedSpotsCollection collection) { + public void addReviewedSpotCollection(ReviewedSpotsCollection collection) throws InterruptedException, ExecutionException { reviewedSpotsCollections.add(collection); + reviewedSpotsCollectionsByIdCache.get().put(collection.getId(), collection); } - public void removeReviewedSpotCollection(ReviewedSpotsCollection collection) { + public void removeReviewedSpotCollection(ReviewedSpotsCollection collection) throws InterruptedException, ExecutionException { reviewedSpotsCollections.remove(collection); + reviewedSpotsCollectionsByIdCache.get().remove(collection.getId()); } } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index aac59e1740b..ab0dbdb938c 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -17,8 +18,8 @@ import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindTracker; -import com.sap.sailing.domain.windfinderadapter.ReviewedSpotsCollection; -import com.sap.sailing.domain.windfinderadapter.Spot; +import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection; +import com.sap.sailing.domain.windfinder.Spot; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -58,10 +59,10 @@ public class WindFinderWindTracker implements WindTracker, Runnable { */ private final Iterable allSpotCollections; - public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory) { + public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory) throws InterruptedException, ExecutionException { this.trackedRace = trackedRace; this.factory = factory; - this.allSpotCollections = factory.getReviewedSpotsCollections(); + this.allSpotCollections = factory.getReviewedSpotsCollections(/* cached */ false); // obtain fresh copy of all spots, updating cache this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this, /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS); } @@ -95,11 +96,11 @@ public class WindFinderWindTracker implements WindTracker, Runnable { } } - private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException { + private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { final Set spots = new HashSet<>(); for (final ReviewedSpotsCollection collection : allSpotCollections) { // TODO bug1301 judge each spot's usefulness given the location of trackedRace - Util.addAll(collection.getSpots(), spots); + Util.addAll(collection.getSpots(/* cached */ false), spots); } return spots; } diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF index adb64e1c158..d7dee890bb9 100755 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -29,6 +29,7 @@ Export-Package: com.sap.sailing.domain.base, com.sap.sailing.domain.trackimport, com.sap.sailing.domain.tracking, com.sap.sailing.domain.tracking.impl, + com.sap.sailing.domain.windfinder, com.sap.sailing.util, com.sap.sailing.util.impl;x-friends:="com.sap.sailing.server.replication,com.sap.sailing.domain.test" Bundle-ActivationPolicy: lazy diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java index 1fa1d107943..74aad516501 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Event.java @@ -45,20 +45,24 @@ public interface Event extends EventBase { void setLeaderboardGroups(Iterable leaderboardGroups); /** - * An event may happen in the vicinity of one or more WindFinder (https://www.windfinder.com) weather - * stations. Which ones those are can be defined using {@link #setWindFinderReviewedSpotsCollection(Iterable)}, - * and this getter returns the IDs last set. + * An event may happen in the vicinity of one or more WindFinder (https://www.windfinder.com) weather stations. + * Which ones those are can be defined using {@link #setWindFinderReviewedSpotsCollection(Iterable)}, and this + * getter returns the IDs last set. + * + * @return an always valid (non-{@code null}) but possibly empty and unmodifiable set of strings; the set is "live," + * meaning it will change as the underlying data changes */ Iterable getWindFinderReviewedSpotsCollectionIds(); /** - * In addition to the wind finder spot collections specified by this event explicitly (see - * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spot collection IDs based - * on the tracked races reachable from this event's associated leaderboard groups and their wind sources. The - * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be - * collected and returned. + * This method may return WindFinder spot IDs based on the tracked races reachable from this event's associated + * leaderboard groups and their wind sources. The {@link WindSource#getId() wind source IDs} of all wind sources of + * type {@link WindSourceType#WINDFINDER} will be collected and returned. + * + * @return an always valid (non-{@code null}) but possibly empty set of strings that is a non-live copy computed + * just for this call */ - Iterable getAllFinderReviewedSpotsCollectionIdsUsedByEvent(); + Iterable getAllFinderSpotIdsUsedByTrackedRacesInEvent(); /** * Set the IDs of the reviewed WindFinder spot collections to consider during this event. diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java index 336544ba3b0..4ba51806e87 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java @@ -78,9 +78,8 @@ public class EventImpl extends EventBaseImpl implements Event { } @Override - public Iterable getAllFinderReviewedSpotsCollectionIdsUsedByEvent() { + public Iterable getAllFinderSpotIdsUsedByTrackedRacesInEvent() { final Set result = new HashSet<>(); - Util.addAll(getWindFinderReviewedSpotsCollectionIds(), result); for (final LeaderboardGroup leaderboardGroup : getLeaderboardGroups()) { for (final Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { for (final TrackedRace trackedRace : leaderboard.getTrackedRaces()) { diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/ReviewedSpotsCollection.java similarity index 56% rename from java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java rename to java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/ReviewedSpotsCollection.java index 9bb0667317d..8a822d55754 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/ReviewedSpotsCollection.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/ReviewedSpotsCollection.java @@ -1,7 +1,8 @@ -package com.sap.sailing.domain.windfinderadapter; +package com.sap.sailing.domain.windfinder; import java.io.IOException; import java.net.MalformedURLException; +import java.util.concurrent.ExecutionException; import org.json.simple.parser.ParseException; @@ -22,5 +23,11 @@ public interface ReviewedSpotsCollection extends WithID { @Override String getId(); - Iterable getSpots() throws MalformedURLException, IOException, ParseException; + /** + * @param cached + * if {@code true}, only those spots collections are returned that this factory has previously obtained; + * this is useful for a very fast lookup but does not guarantee that all spots collections added recently + * will be considered + */ + Iterable getSpots(boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java new file mode 100755 index 00000000000..5cdf0d3ce53 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java @@ -0,0 +1,29 @@ +package com.sap.sailing.domain.windfinder; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.text.ParseException; + +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.windfinder.SpotBase; +import com.sap.sse.common.TimePoint; + +/** + * A measurement spot for which a report and / or a forecast may exist. Equality and + * hash code are based on the {@link #getId() ID} only. + * + * @author Axel Uhl (D043530) + * + */ +public interface Spot extends SpotBase { + Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException; + + Iterable getAllMeasurements() throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException; + + /** + * @param timePoint + * if {@code null} then this call delivers the result of {@link #getAllMeasurements()} + * @return an always valid but possibly empty collection + */ + Iterable getAllMeasurementsAfter(TimePoint timePoint) throws MalformedURLException, IOException, ParseException, org.json.simple.parser.ParseException; +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java new file mode 100755 index 00000000000..fba0e4694fe --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java @@ -0,0 +1,38 @@ +package com.sap.sailing.domain.windfinder; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.concurrent.ExecutionException; + +import org.json.simple.parser.ParseException; + +import com.sap.sailing.domain.tracking.WindTrackerFactory; + +public interface WindFinderTrackerFactory extends WindTrackerFactory { + + /** + * Obtains the reviewed spot collections that this factory knows about. This set is constructed from the collections + * provided explicitly using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and + * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained + * through {@link #reviewedSpotsCollectionIdProvider} if a corresponding service can be resolved. + * + * @param cached + * if {@code true}, only those spots collections are returned that this factory has previously obtained; + * this is useful for a very fast lookup but does not guarantee that all spots collections added recently + * will be considered + * + * @return a non-live set of spots collections known by this factory at this point in time + */ + Iterable getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException; + + ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId) throws InterruptedException, ExecutionException; + + /** + * @param cached + * if {@code true}, only those spots collections are returned that this factory has previously obtained; + * this is useful for a very fast lookup but does not guarantee that all spots collections added recently + * will be considered + */ + Spot getSpotById(String spotId, boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException; + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/windfinder/SpotDTO_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/windfinder/SpotDTO_CustomFieldSerializer.java new file mode 100755 index 00000000000..e3609057e7d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/windfinder/SpotDTO_CustomFieldSerializer.java @@ -0,0 +1,54 @@ +package com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common.windfinder; + +import com.google.gwt.user.client.rpc.CustomFieldSerializer; +import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.client.rpc.SerializationStreamReader; +import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.windfinder.SpotDTO; + +public class SpotDTO_CustomFieldSerializer extends CustomFieldSerializer { + @Override + public boolean hasCustomInstantiateInstance() { + return true; + } + + @Override + public SpotDTO instantiateInstance(SerializationStreamReader streamReader) throws SerializationException { + return instantiate(streamReader); + } + + public static SpotDTO instantiate(SerializationStreamReader streamReader) throws SerializationException { + final String name = streamReader.readString(); + final String id = streamReader.readString(); + final String keyword = streamReader.readString(); + final String englishCountryName = streamReader.readString(); + final Position position = (Position) streamReader.readObject(); + return new SpotDTO(name, id, keyword, englishCountryName, position); + } + + @Override + public void deserializeInstance(SerializationStreamReader streamReader, SpotDTO instance) + throws SerializationException { + deserialize(streamReader, instance); + } + + public static void deserialize(SerializationStreamReader streamReader, SpotDTO instance) { + // handled by instantiate + } + + @Override + public void serializeInstance(SerializationStreamWriter streamWriter, SpotDTO instance) + throws SerializationException { + serialize(streamWriter, instance); + } + + public static void serialize(SerializationStreamWriter streamWriter, SpotDTO instance) + throws SerializationException { + streamWriter.writeString(instance.getName()); + streamWriter.writeString(instance.getId()); + streamWriter.writeString(instance.getKeyword()); + streamWriter.writeString(instance.getEnglishCountryName()); + streamWriter.writeObject(instance.getPosition()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java index c87b41977e4..1c55791e0bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java @@ -4,6 +4,7 @@ import java.util.Date; import java.util.Locale; import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sailing.news.EventNewsService; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.statistics.TrackedRaceStatisticsCache; @@ -16,6 +17,9 @@ import com.sap.sse.gwt.dispatch.client.system.DispatchContext; public interface SailingDispatchContext extends DispatchContext { @GwtIncompatible RacingEventService getRacingEventService(); + + @GwtIncompatible + WindFinderTrackerFactory getWindFinderTrackerFactory(); @GwtIncompatible EventNewsService getEventNewsService(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java index fb97d527461..874ce87ff9a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java @@ -1,14 +1,22 @@ package com.sap.sailing.gwt.home.communication.event; +import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; +import org.json.simple.parser.ParseException; + import com.google.gwt.core.shared.GwtIncompatible; import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.common.dto.EventType; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.windfinder.Spot; import com.sap.sailing.gwt.home.communication.SailingAction; import com.sap.sailing.gwt.home.communication.SailingDispatchContext; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; @@ -62,7 +70,24 @@ public class GetEventViewAction implements SailingAction, IsClient dto.setOfficialWebsiteURL(event.getOfficialWebsiteURL() == null ? null : event.getOfficialWebsiteURL().toString()); URL sailorsInfoWebsiteURL = event.getSailorsInfoWebsiteURLOrFallback(context.getClientLocale()); dto.setSailorsInfoWebsiteURL(sailorsInfoWebsiteURL == null ? null : sailorsInfoWebsiteURL.toString()); - dto.setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(event.getAllFinderReviewedSpotsCollectionIdsUsedByEvent()); + final List windFinderSpots = new ArrayList<>(); + for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { + try { + for (final Spot spot : context.getWindFinderTrackerFactory().getReviewedSpotsCollectionById(spotsCollectionId).getSpots(/* cached */ true)) { + windFinderSpots.add(new SpotDTO(spot)); + } + } catch (IOException | ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spots for reviewed spot collection with ID "+spotsCollectionId); + } + } + for (final String spotIdFromTrackedRace : event.getAllFinderSpotIdsUsedByTrackedRacesInEvent()) { + try { + windFinderSpots.add(new SpotDTO(context.getWindFinderTrackerFactory().getSpotById(spotIdFromTrackedRace, /* cached */ true))); + } catch (IOException | ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spot with ID "+spotIdFromTrackedRace); + } + } + dto.setAllWindFinderSpotsUsedByEvent(windFinderSpots); dto.setHasMedia(HomeServiceUtil.hasMedia(event)); dto.setState(HomeServiceUtil.calculateEventState(event)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java index 1eedc515667..69b1e60286c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/eventview/EventViewDTO.java @@ -9,6 +9,7 @@ import java.util.TreeSet; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.dto.EventType; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.home.communication.event.EventMetadataDTO; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.event.HasLogo; @@ -28,7 +29,7 @@ public class EventViewDTO extends EventMetadataDTO implements Result, HasLogo { private String officialWebsiteURL; private String sailorsInfoWebsiteURL; private String description; - private List allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + private List allWindFinderSpotIdsUsedByEvent; public EventType getType() { return type; @@ -116,26 +117,26 @@ public class EventViewDTO extends EventMetadataDTO implements Result, HasLogo { } /** - * In addition to the wind finder spot collections specified by this event explicitly (see - * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spot collection IDs based + * In addition to the spots from the wind finder spot collections specified by this event explicitly (see + * {@link #getWindFinderReviewedSpotsCollectionIds()}), this method may return additional spots based * on the tracked races reachable from this event's associated leaderboard groups and their wind sources. The * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be - * collected and returned. + * collected and the corresponding {@link SpotDTO} objects are then returned. */ - public Iterable getAllWindFinderReviewedSpotsCollectionIdsUsedByEvent() { - final Iterable result; - if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent == null) { + public Iterable getAllWindFinderSpotIdsUsedByEvent() { + final Iterable result; + if (allWindFinderSpotIdsUsedByEvent == null) { result = Collections.emptySet(); } else { - result = allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + result = allWindFinderSpotIdsUsedByEvent; } return result; } - public void setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(Iterable allWindFinderReviewedSpotsCollectionIdsUsedByEvent) { - this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent = new ArrayList<>(); - if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent != null) { - Util.addAll(allWindFinderReviewedSpotsCollectionIdsUsedByEvent, this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent); + public void setAllWindFinderSpotsUsedByEvent(Iterable windFinderSpots) { + this.allWindFinderSpotIdsUsedByEvent = new ArrayList<>(); + if (windFinderSpots != null) { + Util.addAll(windFinderSpots, this.allWindFinderSpotIdsUsedByEvent); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java index 2a91249fba1..956205892d8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java @@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.shiro.authz.AuthorizationException; import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sailing.gwt.home.communication.SailingDispatchContext; import com.sap.sailing.gwt.server.HomeServiceUtil; import com.sap.sailing.news.EventNewsService; @@ -31,12 +32,15 @@ public class SailingDispatchContextImpl implements SailingDispatchContext { private final SecurityService securityService; private final UserStore userStore; private final TrackedRaceStatisticsCache trackedRaceStatisticsCache; + private final WindFinderTrackerFactory windFinderTrackerFactory; public SailingDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, - EventNewsService eventNewsService, SecurityService securityService, UserStore userStore, - TrackedRaceStatisticsCache trackedRaceStatisticsCache, String clientLocaleName, HttpServletRequest request) { + WindFinderTrackerFactory windFinderTrackerFactory, EventNewsService eventNewsService, + SecurityService securityService, UserStore userStore, TrackedRaceStatisticsCache trackedRaceStatisticsCache, + String clientLocaleName, HttpServletRequest request) { this.currentClientTime = currentClientTime; this.racingEventService = racingEventService; + this.windFinderTrackerFactory = windFinderTrackerFactory; this.eventNewsService = eventNewsService; this.securityService = securityService; this.userStore = userStore; @@ -50,6 +54,11 @@ public class SailingDispatchContextImpl implements SailingDispatchContext { return racingEventService; } + @Override + public WindFinderTrackerFactory getWindFinderTrackerFactory() { + return windFinderTrackerFactory; + } + public EventNewsService getEventNewsService() { return eventNewsService; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java index 1d4b7468753..323139e954c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java @@ -3,6 +3,7 @@ package com.sap.sailing.gwt.home.server.servlets; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sailing.gwt.home.communication.SailingDispatchContext; import com.sap.sailing.gwt.home.server.SailingDispatchContextImpl; import com.sap.sailing.gwt.ui.server.Activator; @@ -21,6 +22,7 @@ public class SailingDispatchServlet extends AbstractDispatchServlet racingEventServiceTracker; + private final ServiceTracker windFinderTrackerFactory; private final ServiceTracker eventNewsServiceTracker; private final ServiceTracker securityServiceTracker; private final ServiceTracker userStoreTracker; @@ -29,6 +31,7 @@ public class SailingDispatchServlet extends AbstractDispatchServlet> SailingDispatchContext createDispatchContextFor( RequestWrapper request) { return new SailingDispatchContextImpl(request.getCurrentClientTime(), racingEventServiceTracker.getService(), + windFinderTrackerFactory.getService(), eventNewsServiceTracker.getService(), securityServiceTracker.getService(), userStoreTracker.getService(), trackedRaceStatisticsCacheTracker.getService(), request.getClientLocaleName(), getThreadLocalRequest()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index dcd8ea67d52..c10c7656fa6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -48,6 +48,7 @@ import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogExcep import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; import com.sap.sailing.domain.common.racelog.tracking.TransformationException; import com.sap.sailing.domain.common.tracking.impl.PreciseCompactGPSFixMovingImpl.PreciseCompactPosition; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.tracking.TrackedRace; @@ -711,5 +712,5 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS List getRaceDisplayNamesFromLeaderboard(String leaderboardName, List raceColumnNames) throws NotFoundException; - String getWindFinderLink(TimePoint forWhichTime, String spotId) throws Exception; + SpotDTO getWindFinderSpot(String spotId) throws Exception; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index a49428d9403..36d89bca854 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -41,6 +41,7 @@ import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.domain.common.tracking.impl.PreciseCompactGPSFixMovingImpl.PreciseCompactPosition; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.expeditionconnector.ExpeditionDeviceConfiguration; import com.sap.sailing.gwt.ui.adminconsole.RaceLogSetTrackingTimesDTO; @@ -936,5 +937,5 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan * is up to ten days in the future, or the statistics page if the time point is out of any * of the scopes above. */ - void getWindFinderLink(TimePoint forWhichTime, String spotId, AsyncCallback callback); + void getWindFinderSpot(String spotId, AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 212a31e4136..f9c435c2309 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -86,6 +86,7 @@ import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.MeterDistance; import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.ui.actions.GetBoatPositionsAction; import com.sap.sailing.gwt.ui.actions.GetPolarAction; import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; @@ -2215,18 +2216,25 @@ public class RaceMap extends AbstractCompositeComponent impleme final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.asDate().toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsSignedDecimalDegrees())); + Label positionInDecimalDegreesLabel = new Label(" "+windDTO.position.getAsSignedDecimalDegrees()); + positionInDecimalDegreesLabel.setWordWrap(false); + positionInDecimalDegreesLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + positionInDecimalDegreesLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); + positionInDecimalDegreesLabel.getElement().getStyle().setFontWeight(Style.FontWeight.LIGHTER); + positionInDecimalDegreesLabel.getElement().getStyle().setFontSize(0.7, Unit.EM); + vPanel.add(positionInDecimalDegreesLabel); if (windSource.getType() == WindSourceType.WINDFINDER) { - sailingService.getWindFinderLink(timePoint, windSource.getId().toString(), new AsyncCallback() { + sailingService.getWindFinderSpot(windSource.getId().toString(), new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError(stringMessages.unableToResolveWindFinderSpotId(windSource.getId().toString(), caught.getMessage()), /* silentMode */ true); } @Override - public void onSuccess(String result) { - // TODO bug1301 place WindFinder button icon on anchor - windSourceNameAnchor.setHref(result); + public void onSuccess(SpotDTO result) { + // TODO bug1301 place WindFinder button icon on anchor and format nicely; this or similar could be the image icon to use: RaceMap.this.raceMapImageManager.getWindFinderIconSmall(); + windSourceNameAnchor.setHref(result.getCurrentlyMostAppropriateUrl(timePoint)); + windSourceNameAnchor.setName(stringMessages.windFinder()+": "+result.getName()+" ("+result.getId()+")"); windSourceNameAnchor.setTarget("_blank"); } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java index 274d05663a7..280bbfc5a01 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java @@ -91,4 +91,8 @@ public class RaceMapImageManager { public ImageTransformer getWindSensorIconTransformer() { return windSensorIconTransformer; } + + public ImageResource getWindFinderIconSmall() { + return resources.getWindFinderIconSmall(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java index 7c7d2fdae04..7482c8385f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java @@ -27,6 +27,9 @@ public interface RaceMapResources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/maneuver_green.png") ImageResource maneuverMarkerGreen(); + + @Source("com/sap/sailing/gwt/ui/client/images/windfinder_small.png") + ImageResource getWindFinderIconSmall(); @Source("RaceMap.css") RaceMapStyle raceMapStyle(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index f2fecdc4c22..29e698555ad 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -57,6 +57,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; + import com.sap.sailing.competitorimport.CompetitorProvider; import com.sap.sailing.domain.abstractlog.AbstractLog; import com.sap.sailing.domain.abstractlog.AbstractLogEvent; @@ -241,6 +242,7 @@ import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.common.tracking.impl.GPSFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; import com.sap.sailing.domain.common.tracking.impl.PreciseCompactGPSFixMovingImpl.PreciseCompactPosition; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.domain.igtimiadapter.Account; import com.sap.sailing.domain.igtimiadapter.IgtimiConnection; import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory; @@ -311,8 +313,8 @@ import com.sap.sailing.domain.tractracadapter.TracTracAdapter; import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; -import com.sap.sailing.domain.windfinderadapter.Spot; -import com.sap.sailing.domain.windfinderadapter.WindFinderTrackerFactory; +import com.sap.sailing.domain.windfinder.Spot; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sailing.expeditionconnector.ExpeditionDeviceConfiguration; import com.sap.sailing.expeditionconnector.ExpeditionSensorDeviceIdentifier; import com.sap.sailing.expeditionconnector.ExpeditionTrackerFactory; @@ -3922,7 +3924,27 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S eventDTO.addLeaderboardGroup(convertToLeaderboardGroupDTO(lg, /* withGeoLocationData */ false, withStatisticalData)); } eventDTO.setWindFinderReviewedSpotsCollection(event.getWindFinderReviewedSpotsCollectionIds()); - eventDTO.setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(event.getAllFinderReviewedSpotsCollectionIdsUsedByEvent()); + final WindFinderTrackerFactory windFinderTrackerFactory = windFinderTrackerFactoryServiceTracker.getService(); + final List windFinderSpots = new ArrayList<>(); + if (windFinderTrackerFactory != null) { + for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { + try { + for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId).getSpots(/* cached */ false)) { + windFinderSpots.add(new SpotDTO(spot)); + } + } catch (IOException | org.json.simple.parser.ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spots for reviewed spot collection with ID "+spotsCollectionId); + } + } + for (final String spotIdFromTrackedRace : event.getAllFinderSpotIdsUsedByTrackedRacesInEvent()) { + try { + windFinderSpots.add(new SpotDTO(windFinderTrackerFactory.getSpotById(spotIdFromTrackedRace, /* cached */ false))); + } catch (IOException | org.json.simple.parser.ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spot with ID "+spotIdFromTrackedRace); + } + } + eventDTO.setAllWindFinderSpotsUsedByEvent(windFinderSpots); + } return eventDTO; } @@ -6958,19 +6980,19 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } @Override - public String getWindFinderLink(TimePoint forWhichTime, String spotId) throws MalformedURLException, IOException, org.json.simple.parser.ParseException { - final URL result; + public SpotDTO getWindFinderSpot(String spotId) throws MalformedURLException, IOException, org.json.simple.parser.ParseException, InterruptedException, ExecutionException { + final SpotDTO result; final WindFinderTrackerFactory windFinderTrackerFactory = windFinderTrackerFactoryServiceTracker.getService(); if (windFinderTrackerFactory != null) { - final Spot spot = windFinderTrackerFactory.getSpotById(spotId); + final Spot spot = windFinderTrackerFactory.getSpotById(spotId, /* cached */ false); if (spot != null) { - result = spot.getCurrentlyMostAppropriateUrl(forWhichTime); + result = new SpotDTO(spot); } else { result = null; } } else { result = null; } - return result.toString(); + return result; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java index ba22aeeb7a5..b8fc96041f3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java @@ -9,6 +9,7 @@ import java.util.UUID; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sse.common.Util; public class EventDTO extends EventBaseDTO { @@ -19,7 +20,7 @@ public class EventDTO extends EventBaseDTO { private List leaderboardGroups; // keeps the more specific type accessible in a type-safe way private List windFinderReviewedSpotsCollectionIds; - private List allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + private List allWindFinderSpotIdsUsedByEvent; public EventDTO() { this(new ArrayList()); @@ -124,20 +125,20 @@ public class EventDTO extends EventBaseDTO { * {@link WindSource#getId() wind source IDs} of all wind sources of type {@link WindSourceType#WINDFINDER} will be * collected and returned. */ - public Iterable getAllWindFinderReviewedSpotsCollectionIdsUsedByEvent() { - final Iterable result; - if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent == null) { + public Iterable getAllWindFinderSpotIdsUsedByEvent() { + final Iterable result; + if (allWindFinderSpotIdsUsedByEvent == null) { result = Collections.emptySet(); } else { - result = allWindFinderReviewedSpotsCollectionIdsUsedByEvent; + result = allWindFinderSpotIdsUsedByEvent; } return result; } - public void setAllWindFinderReviewedSpotsCollectionIdsUsedByEvent(Iterable allWindFinderReviewedSpotsCollectionIdsUsedByEvent) { - this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent = new ArrayList<>(); - if (allWindFinderReviewedSpotsCollectionIdsUsedByEvent != null) { - Util.addAll(allWindFinderReviewedSpotsCollectionIdsUsedByEvent, this.allWindFinderReviewedSpotsCollectionIdsUsedByEvent); + public void setAllWindFinderSpotsUsedByEvent(List windFinderSpots) { + this.allWindFinderSpotIdsUsedByEvent = new ArrayList<>(); + if (windFinderSpots != null) { + Util.addAll(windFinderSpots, this.allWindFinderSpotIdsUsedByEvent); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder_small.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder_small.png new file mode 100755 index 0000000000000000000000000000000000000000..0095f496bf0f3d68e7acbfbe72c4ea14cd5826ff GIT binary patch literal 23160 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 Date: Mon, 8 Jan 2018 21:48:13 +0100 Subject: [PATCH 030/214] fixed threading / tasking for WindFinder cache initialization Change-Id: I10ec4369f1a42ea5ba5d3058733bfb58df71fc7a --- .../impl/WindFinderTrackerFactoryImpl.java | 25 +++++++++++-------- .../windfinder/WindFinderTrackerFactory.java | 2 +- .../event/GetEventViewAction.java | 3 ++- .../gwt/ui/server/SailingServiceImpl.java | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java index 828f5a943e0..a0e8db8f37f 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java @@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.json.simple.parser.ParseException; @@ -45,17 +46,20 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { */ private Set reviewedSpotsCollections; private final ServiceTracker reviewedSpotsCollectionIdProvider; + + private final ScheduledExecutorService executor; public WindFinderTrackerFactoryImpl() { this.windTrackerPerRace = new HashMap<>(); + this.executor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(); this.reviewedSpotsCollections = Collections.synchronizedSet(new HashSet<>()); if (Activator.getContext() != null) { reviewedSpotsCollectionIdProvider = ServiceTrackerFactory.createAndOpen(Activator.getContext(), WindFinderReviewedSpotsCollectionIdProvider.class); - reviewedSpotsCollectionsByIdCache = ThreadPoolUtil.INSTANCE.createBackgroundTaskThreadPoolExecutor(1, getClass().getName()+" init").schedule( + reviewedSpotsCollectionsByIdCache = executor.schedule( ()->{ final ConcurrentMap result = new ConcurrentHashMap<>(); reviewedSpotsCollectionIdProvider.getService(); - for (final ReviewedSpotsCollection c : loadSpotCollectionsFromProvider()) { + for (final ReviewedSpotsCollection c : loadSpotCollectionsFromProvider(/* lookupInCache */ false)) { result.put(c.getId(), c); } return result; @@ -110,7 +114,7 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { result.addAll(reviewedSpotsCollectionsByIdCache.get().values()); } else { result.addAll(reviewedSpotsCollections); - Util.addAll(loadSpotCollectionsFromProvider(), result); + Util.addAll(loadSpotCollectionsFromProvider(/* lookupInCache */ true), result); for (final ReviewedSpotsCollection c : result) { reviewedSpotsCollectionsByIdCache.get().putIfAbsent(c.getId(), c); } @@ -118,23 +122,24 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory { return result; } - private Iterable loadSpotCollectionsFromProvider() throws InterruptedException, ExecutionException { + private Iterable loadSpotCollectionsFromProvider(boolean lookupInCache) throws InterruptedException, ExecutionException { final Set result = new HashSet<>(); final WindFinderReviewedSpotsCollectionIdProvider provider; if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) { for (String id : provider.getWindFinderReviewedSpotsCollectionIds()) { - result.add(getReviewedSpotsCollectionById(id)); + result.add(getReviewedSpotsCollectionById(id, lookupInCache)); } } return result; } @Override - public ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId) throws InterruptedException, ExecutionException { - ReviewedSpotsCollection result = reviewedSpotsCollectionsByIdCache.get().get(spotsCollectionId); - if (result == null) { - result = new ReviewedSpotsCollectionImpl(spotsCollectionId); - reviewedSpotsCollectionsByIdCache.get().put(spotsCollectionId, result); + public ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId, boolean lookupInCache) throws InterruptedException, ExecutionException { + ReviewedSpotsCollection result; + if (!lookupInCache || (result = reviewedSpotsCollectionsByIdCache.get().get(spotsCollectionId)) == null) { + final ReviewedSpotsCollectionImpl finalResult = new ReviewedSpotsCollectionImpl(spotsCollectionId); + result = finalResult; + executor.schedule(()->reviewedSpotsCollectionsByIdCache.get().put(spotsCollectionId, finalResult), /* delay */ 0, TimeUnit.MILLISECONDS); } return result; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java index fba0e4694fe..e48885bb85c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java @@ -25,7 +25,7 @@ public interface WindFinderTrackerFactory extends WindTrackerFactory { */ Iterable getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException; - ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId) throws InterruptedException, ExecutionException; + ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId, boolean lookupInCache) throws InterruptedException, ExecutionException; /** * @param cached diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java index 874ce87ff9a..7d0fda1934e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java @@ -73,7 +73,8 @@ public class GetEventViewAction implements SailingAction, IsClient final List windFinderSpots = new ArrayList<>(); for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { try { - for (final Spot spot : context.getWindFinderTrackerFactory().getReviewedSpotsCollectionById(spotsCollectionId).getSpots(/* cached */ true)) { + for (final Spot spot : context.getWindFinderTrackerFactory().getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). + getSpots(/* cached */ true)) { windFinderSpots.add(new SpotDTO(spot)); } } catch (IOException | ParseException | InterruptedException | ExecutionException e) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 29e698555ad..1af9bf3ab6f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3929,7 +3929,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S if (windFinderTrackerFactory != null) { for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { try { - for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId).getSpots(/* cached */ false)) { + for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). + getSpots(/* cached */ false)) { windFinderSpots.add(new SpotDTO(spot)); } } catch (IOException | org.json.simple.parser.ParseException | InterruptedException | ExecutionException e) { From 079ef49f798658765b8c9b8fb73253fa3631792e Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 9 Jan 2018 14:45:25 +0100 Subject: [PATCH 031/214] Changed domain name from dummy2.sapsailing.com to sapsailing.com --- configuration/aws-automation/lib/functions_elb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index a03826fa256..6253c9ea62f 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -59,7 +59,7 @@ function create_rule(){ local_echo "Creating rule for listener..." local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.dummy2.sapsailing.com" + local domain="$subdomain.sapsailing.com" aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3 1>&2 echo $domain From b1a2cb7a71744772fa99b207964a25975edf4682 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 9 Jan 2018 14:58:22 +0100 Subject: [PATCH 032/214] Removed elastic ip scenario and classic load balancer scenario. Removed route53 methods. Adjusted script help. --- configuration/aws-automation/aws-setup.sh | 33 ++------------- .../aws-automation/lib/functions_elb.sh | 41 ------------------- .../aws-automation/lib/functions_route53.sh | 29 ------------- .../lib/scenario_associate_clb.sh | 37 ----------------- .../lib/scenario_associate_elastic_ip.sh | 34 --------------- 5 files changed, 4 insertions(+), 170 deletions(-) delete mode 100644 configuration/aws-automation/lib/functions_route53.sh delete mode 100644 configuration/aws-automation/lib/scenario_associate_clb.sh delete mode 100644 configuration/aws-automation/lib/scenario_associate_elastic_ip.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index d27c8dc5915..93dcaaf0ad3 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -74,12 +74,6 @@ fi if $instance; then instance_start - if $associate_clb; then - associate_clb_start - fi - if $associate_elastic_ip; then - associate_elastic_ip_start - fi if $associate_alb; then associate_alb_start fi @@ -132,9 +126,6 @@ usage() { --associate-alb Associate instance with existing application load balancer whos listener is defined in variables_aws.sh. Automatically create necessary target group and host name rule. - Currently *.dummy.sapsailing.com is used for test purposes. - --associate-clb Associate instance with new classic load balancer. - --associate-elastic-ip Associate instance with new elastic ip. --tail Tail logs from instance using tmux ${bold}Other:${reset} @@ -148,12 +139,9 @@ usage() { Associate application load balancer: > ./aws-setup.sh --instance --associate-alb - Associate instance with classic load balancer while + Associate instance with application load balancer while automatically tailing important log files (tmux required): - > ./aws-setup.sh --instance --associate-clb --tail - - Associate instance with elastic ip: - > ./aws-setup.sh --instance --associate-elastic-ip + > ./aws-setup.sh --instance --associate-alb --tail Tail logfiles of running instance with dns name: > ./aws-setup.sh --tail --public-dns-name ec2-x.compute.amazonaws.com @@ -162,17 +150,8 @@ usage() { > ./aws-setup.sh --instance --force Create instance and use default values except instance name - and instance short name: - > ./aws-setup.sh --instance --instance-name Test --instance-short-name t --force - - Associate instance with classic load balancer by - passing all relevant parameters to script. - Also use debug mode. - > ./aws-setup.sh --region eu-west-2 --instance-type t2.medium - --key-name leonradeck-keypair --key-file /cygdrive/c/Users/d069485/ - .ssh/leonradeck-keypair.pem --user-username test --user-password test - --instance-name \"WC Santander 2017\" --instance-short-name test - --new-admin-password admin -d --instance-with-load-balancer + and instance short name. Also activate debug mode. + > ./aws-setup.sh --instance --instance-name Test --instance-short-name t --force -d " @@ -219,9 +198,7 @@ unset options [[ $# -eq 0 ]] && set -- "--help" # Set default value of variable without parameter value to false -associate_clb=false associate_alb=false -associate_elastic_ip=false instance=false sub_instance=false tail=false @@ -250,9 +227,7 @@ while [[ $1 = -?* ]]; do -d|--debug) debug=true ;; --instance) instance=true ;; --sub-instance) sub_instance=true ;; - --associate-clb) associate_clb=true ;; --associate-alb) associate_alb=true ;; - --associate-elastic-ip) associate_elastic_ip=true ;; --tail) tail=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 6253c9ea62f..42e8b285ddf 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -79,44 +79,3 @@ function get_rule_with_highest_priority(){ echo 1 fi } - -# NOT TESTED -# ----------------------------------------------------------- -# Create load balancer with HTTPS rule -# @param $1 load_balancer_name -# @param $2 certificate arn -# @param $2 security group ids -# @return json result -# ----------------------------------------------------------- -function create_load_balancer_https(){ - local_echo "Creating load balancer \"$load_balancer_name\"..." - local load_balancer_name=$(alphanumeric $1) - aws_wrapper elb create-load-balancer --load-balancer-name $load_balancer_name \ - --listeners "Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80" \ - "Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=$2" \ - --availability-zones "$(get_availability_zones)" --security-groups "$elb_security_group_ids" -} - -# ----------------------------------------------------------- -# Configure health check HTTP -# @param $1 load balancer name -# @return json result -# ----------------------------------------------------------- -function configure_health_check_http(){ - local_echo "Configuring load balancer health check..." - local load_balancer_name=$(alphanumeric $1) - aws_wrapper elb configure-health-check --load-balancer-name $load_balancer_name \ - --health-check Target=HTTP:80,Interval=5,UnhealthyThreshold=2,HealthyThreshold=2,Timeout=4 -} - -# ----------------------------------------------------------- -# Add instance to load balancer -# @param $1 load balancer name -# @param $2 instance id -# @return json result -# ----------------------------------------------------------- -function add_instance_to_clb(){ - local_echo "Adding instance to clb..." - local load_balancer_name=$(alphanumeric $1) - aws_wrapper elb register-instances-with-load-balancer --load-balancer-name $load_balancer_name --instances $2 -} diff --git a/configuration/aws-automation/lib/functions_route53.sh b/configuration/aws-automation/lib/functions_route53.sh deleted file mode 100644 index 8abd9aac8cb..00000000000 --- a/configuration/aws-automation/lib/functions_route53.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -# NOT TESTED - -# ----------------------------------------------------------- -# Create CNAME record for the hosted zone to forward to a load balancer -# @param $1 subdomain name -# @param $2 TTL -# @param $3 load balancer dns name -# @return json result -# ----------------------------------------------------------- -function route53_change_resource_record(){ - create_route53_record_file $1 $2 $3 - local_echo "Creating Route53 record set (Name: $1.sapsailing.com Value: $3 Type: CNAME)..." - aws_wrapper route53 change-resource-record-sets --hosted-zone-id "$hosted_zone_id" \ - --change-batch "file://${tmpDir}/$change_resource_record_set_file" -} - -# ----------------------------------------------------------- -# Creates a file which includes the record change as json -# @param $1 subdomain name -# @param $2 TTL -# @param $3 load balancer dns name -# ----------------------------------------------------------- -function create_route53_record_file(){ - local json=$(printf '{"Changes":[{"Action":"CREATE","ResourceRecordSet": \ - {"Name": "%s.sapsailing.com","Type": "CNAME","TTL": %s,"ResourceRecords":[{"Value": "%s"}]}}]}' $1 $2 $3) - echo "$json" > "${tmpDir}/$change_resource_record_set_file" -} diff --git a/configuration/aws-automation/lib/scenario_associate_clb.sh b/configuration/aws-automation/lib/scenario_associate_clb.sh deleted file mode 100644 index 65146cde589..00000000000 --- a/configuration/aws-automation/lib/scenario_associate_clb.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# Create classic load balancer for instance -# ------------------------------------------------------ - -function associate_clb_start(){ - associate_clb_require - associate_clb_execute -} - -# ----------------------------------------------------------- -# All these variables are needed for this scenario -# If one variable is not assigned or passed by parameter -# the user will be prompted to enter a value -# ----------------------------------------------------------- -function associate_clb_require(){ - # will only be executed after instance creation - : -} - -function associate_clb_execute() { - header "Associating classic load balancer" - - # create classic load balancer - local load_balancer_dns_name=$(create_load_balancer_https $instance_name $certificate_arn | get_attribute '.DNSName') - - # configure health check of load balancer - local json_health_check=$(configure_health_check_https $instance_name) - - # add instance to clb - local json_added_instance_response=$(add_instance_to_clb $instance_name $instance_id) - - header "Apache configuration" - - # configure apache with event macro - append_event_ssl_macro_to_001_events_conf $public_dns_name $event_id $ssh_user $public_dns_name '8888' -} diff --git a/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh b/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh deleted file mode 100644 index 2ced373d8d9..00000000000 --- a/configuration/aws-automation/lib/scenario_associate_elastic_ip.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# Create elastic ip and associate it with instance -# ------------------------------------------------------ - -function associate_elastic_ip_start(){ - associate_elastic_ip_require - associate_elastic_ip_execute -} - -# ----------------------------------------------------------- -# All these variables are needed for this scenario -# If one variable is not assigned or passed by parameter -# the user will be prompted to enter a value -# ----------------------------------------------------------- -function associate_elastic_ip_require(){ -# will only be executed after instance creation - : -} - -function associate_elastic_ip_execute() { - header "Elastic ip creation" - - # allocate elastic ip - local elastic_ip=$(allocate_address) - - # associate elastic ip - associate_address $instance_id $elastic_ip - - header "Apache configuration" - - # configure apache with event macro - append_event_ssl_macro_to_001_events_conf $elastic_ip $event_id $ssh_user $elastic_ip '8888' -} From b39c2db92d0367a17a1d139c506da0c9dc146c24 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 Jan 2018 14:49:05 +0100 Subject: [PATCH 033/214] Bug 4380: First concept for race slicing backend --- .../sailing/gwt/ui/client/SailingService.java | 5 + .../gwt/ui/client/SailingServiceAsync.java | 6 + .../gwt/ui/server/SailingServiceImpl.java | 110 ++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 5ee4920d4b3..9fb961a7910 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -105,6 +105,7 @@ import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; @@ -709,4 +710,8 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS throws NotFoundException, CompetitorRegistrationOnRaceLogDisabledException; List getRaceDisplayNamesFromLeaderboard(String leaderboardName, List raceColumnNames) throws NotFoundException; + + boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier); + + RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimeRange timeRange); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index c179402ea0e..1f316ca19ab 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -95,6 +95,7 @@ import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; @@ -927,4 +928,9 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan final Iterable selectedFlightNames,PairingListDTO pairingListDTO, AsyncCallback callback); void getRaceDisplayNamesFromLeaderboard(final String leaderboardName,List raceColumnNames, AsyncCallback> callback); + + void canSliceRace(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); + + void sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimeRange timeRange, + AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 1f5a127097e..70f693d5dfe 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -57,9 +57,11 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; + import com.sap.sailing.competitorimport.CompetitorProvider; import com.sap.sailing.domain.abstractlog.AbstractLog; import com.sap.sailing.domain.abstractlog.AbstractLogEvent; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.AllEventsOfTypeFinder; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; @@ -69,6 +71,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.AbortingFlagFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.LastPublishedCourseDesignFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.MarkPassingDataFinder; @@ -79,13 +82,18 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImp import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogSuppressedMarkPassingsEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.FlagPoleState; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.gate.ReadonlyGateStartRacingProcedure; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.ConfigurableStartModeFlagRacingProcedure; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogRegisterCompetitorEvent; import com.sap.sailing.domain.abstractlog.race.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; +import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogDenoteForTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogRegisterCompetitorEventImpl; +import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogStartTrackingEventImpl; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; @@ -296,6 +304,7 @@ import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.Maneuver; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.Track; import com.sap.sailing.domain.tracking.TrackedLeg; @@ -6946,4 +6955,105 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } return result; } + + @Override + public boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier) { + final Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); + if (Util.size(regatta.getSeries()) != 1) { + return false; + } + for (Series series : regatta.getSeries()) { + if (Util.size(series.getFleets()) != 1) { + return false; + } + } + final DynamicTrackedRace trackedRace = getService().getTrackedRace(raceIdentifier); + if (trackedRace == null || trackedRace.getStartOfTracking() == null) { + return false; + } + return true; + } + + @Override + public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, + TimeRange timeRange) { + if (!canSliceRace(raceIdentifier)) { + throw new RuntimeException("Can not slice race"); + } + final String trackedRaceName = newRaceColumnName; + final RegattaIdentifier regattaIdentifier = new RegattaName(raceIdentifier.getRegattaName()); + final Regatta regatta = getService().getRegatta(regattaIdentifier); + final Series series = regatta.getSeries().iterator().next(); + final Fleet fleet = series.getFleets().iterator().next(); + final DynamicTrackedRace trackedRaceToSlice = getService().getTrackedRace(raceIdentifier); + final TimePoint startOfTrackingOfRaceToSlice = trackedRaceToSlice.getStartOfTracking(); + final TimePoint endOfTrackingOfRaceToSlice = trackedRaceToSlice.getEndOfTracking(); + if (startOfTrackingOfRaceToSlice.after(timeRange.from()) || (endOfTrackingOfRaceToSlice != null && endOfTrackingOfRaceToSlice.before(timeRange.to()))) { + throw new RuntimeException("The TimeRange to slice is not part of the race"); + } + + getService().apply(new AddColumnToSeries(regattaIdentifier, series.getName(), newRaceColumnName)); + final RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) getService().getLeaderboardByName(raceIdentifier.getRegattaName()); + + final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRaceToSlice); + final RaceColumn raceColumnOfRaceToSlice = raceColumnAndFleetOfRaceToSlice.getA(); + final RaceLog raceLogOfRaceToSlice = raceColumnOfRaceToSlice.getRaceLog(fleet); + + final RaceColumn raceColumn = regattaLeaderboard.getRaceColumnByName(newRaceColumnName); + + final RaceLog raceLog = raceColumn.getRaceLog(fleet); + + final AbstractLogEventAuthor author = getService().getServerAuthor(); + + final TimePoint startOfTracking = timeRange.from(); + final TimePoint endOfTracking = timeRange.to(); + raceLog.add(new RaceLogStartOfTrackingEventImpl(startOfTracking, author, raceLog.getCurrentPassId())); + raceLog.add(new RaceLogEndOfTrackingEventImpl(endOfTracking, author, raceLog.getCurrentPassId())); + + for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { + if (raceLogEvent instanceof RaceLogRegisterCompetitorEvent) { + final RaceLogRegisterCompetitorEvent raceLogRegisterCompetitorEvent = (RaceLogRegisterCompetitorEvent) raceLogEvent; + raceLog.add(new RaceLogRegisterCompetitorEventImpl(startOfTracking, author, raceLog.getCurrentPassId(), + raceLogRegisterCompetitorEvent.getCompetitor())); + } + if (raceLogEvent instanceof RaceLogWindFixEvent) { + final RaceLogWindFixEvent raceLogWindFixEvent = (RaceLogWindFixEvent) raceLogEvent; + final Wind windFix = raceLogWindFixEvent.getWindFix(); + if (timeRange.includes(windFix.getTimePoint())) { + raceLog.add(new RaceLogWindFixEventImpl(windFix.getTimePoint(), author, raceLog.getCurrentPassId(), windFix, raceLogWindFixEvent.isMagnetic())); + } + } + // TODO do we need to copy more events? + } + + try { + TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); + // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis + // that's why we can't just call adapter.denoteRaceForRaceLogTracking + final TimePoint denotationTimePoint = startTrackingTimePoint.minus(1); + raceLog.add(new RaceLogDenoteForTrackingEventImpl(denotationTimePoint, + author, raceLog.getCurrentPassId(), trackedRaceName, regatta.getBoatClass(), UUID.randomUUID())); + + raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); + + final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, + raceColumn, fleet, /* trackWind */true, /* correctWindDirectionByMagneticDeclination */ true); + + // TODO do we need to wait or is the TrackedRace guaranteed to be reachable after calling startTracking? + raceHandle.getRace(); + + final DynamicTrackedRace trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); + + for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources(WindSourceType.EXPEDITION)) { + final WindTrack windTrackToCopyFrom = trackedRaceToSlice.getOrCreateWindTrack(windSourceToCopy); + for (Wind windToCopy : windTrackToCopyFrom.getFixes(startOfTracking, true, endOfTracking, true)) { + trackedRace.recordWind(windToCopy, windSourceToCopy); + } + } + return trackedRace.getRaceIdentifier(); + } catch (Exception e) { + throw new RuntimeException("Error while slicing race"); + } + + } } From f713f3631a4fcb7484b00e4ab7cd349c93ac980d Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 9 Jan 2018 15:10:50 +0100 Subject: [PATCH 034/214] Remove temporary directory creation. --- configuration/aws-automation/aws-setup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 93dcaaf0ad3..cbdda789a67 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -52,10 +52,10 @@ args=() # Create temp directory with three random numbers and the process ID # in the name. This directory is removed automatically at exit. -tmpDir="./tmp.$RANDOM.$RANDOM.$RANDOM.$$/" -(umask 000 && mkdir "${tmpDir}") || { - die "Could not create temporary directory! Exiting." -} +# tmpDir="./tmp.$RANDOM.$RANDOM.$RANDOM.$$/" +# (umask 000 && mkdir "${tmpDir}") || { + # die "Could not create temporary directory! Exiting." +# } # Logging (not used) # To never save a logfile change variable to '/dev/null' From a148993fad1d2ff5832a4075d5ad8d0e4a57b80f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 9 Jan 2018 15:12:45 +0100 Subject: [PATCH 035/214] Reintroduced waiting step for ssh connection --- configuration/aws-automation/lib/scenario_instance.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index cae6076c5a8..a72e08be444 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -38,11 +38,14 @@ function instance_execute() { # get public dns name of instance public_dns_name=$(query_public_dns_name $instance_id) + wait_for_ssh_connection $ssh_user $public_dns_name + # if --tail option is passed then tail logfiles of instance within tmux if $tail; then tail_start fi + header "Event and user creation" local port="8888" From 1b1f40422dd9730edb47189dd9fccbc599e65014 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 Jan 2018 15:39:21 +0100 Subject: [PATCH 036/214] Bug 4380: Adjustments to support race slicing in more cases --- .../gwt/ui/server/SailingServiceImpl.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 70f693d5dfe..599db15cf65 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -6959,16 +6959,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier) { final Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); - if (Util.size(regatta.getSeries()) != 1) { + final Leaderboard regattaLeaderboard = getService().getLeaderboardByName(raceIdentifier.getRegattaName()); + final DynamicTrackedRace trackedRace = getService().getTrackedRace(raceIdentifier); + if (regatta == null || !(regattaLeaderboard instanceof RegattaLeaderboard) || trackedRace == null || trackedRace.getStartOfTracking() == null) { return false; } - for (Series series : regatta.getSeries()) { - if (Util.size(series.getFleets()) != 1) { - return false; - } - } - final DynamicTrackedRace trackedRace = getService().getTrackedRace(raceIdentifier); - if (trackedRace == null || trackedRace.getStartOfTracking() == null) { + + final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRace); + if (raceColumnAndFleetOfRaceToSlice == null) { + // the TrackedRace is not associated to the given RegattaLeaderboard return false; } return true; @@ -6983,8 +6982,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final String trackedRaceName = newRaceColumnName; final RegattaIdentifier regattaIdentifier = new RegattaName(raceIdentifier.getRegattaName()); final Regatta regatta = getService().getRegatta(regattaIdentifier); - final Series series = regatta.getSeries().iterator().next(); - final Fleet fleet = series.getFleets().iterator().next(); final DynamicTrackedRace trackedRaceToSlice = getService().getTrackedRace(raceIdentifier); final TimePoint startOfTrackingOfRaceToSlice = trackedRaceToSlice.getStartOfTracking(); final TimePoint endOfTrackingOfRaceToSlice = trackedRaceToSlice.getEndOfTracking(); @@ -6992,13 +6989,16 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S throw new RuntimeException("The TimeRange to slice is not part of the race"); } - getService().apply(new AddColumnToSeries(regattaIdentifier, series.getName(), newRaceColumnName)); final RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) getService().getLeaderboardByName(raceIdentifier.getRegattaName()); final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRaceToSlice); - final RaceColumn raceColumnOfRaceToSlice = raceColumnAndFleetOfRaceToSlice.getA(); + // RaceColumns in a RegattaLeaderboard are always RaceColumnInSeries instances + final RaceColumnInSeries raceColumnOfRaceToSlice = (RaceColumnInSeries) raceColumnAndFleetOfRaceToSlice.getA(); + final Fleet fleet = raceColumnAndFleetOfRaceToSlice.getB(); + final Series series = raceColumnOfRaceToSlice.getSeries(); final RaceLog raceLogOfRaceToSlice = raceColumnOfRaceToSlice.getRaceLog(fleet); + getService().apply(new AddColumnToSeries(regattaIdentifier, series.getName(), newRaceColumnName)); final RaceColumn raceColumn = regattaLeaderboard.getRaceColumnByName(newRaceColumnName); final RaceLog raceLog = raceColumn.getRaceLog(fleet); @@ -7026,18 +7026,18 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S // TODO do we need to copy more events? } + final TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); + // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis + // that's why we can't just call adapter.denoteRaceForRaceLogTracking + final TimePoint denotationTimePoint = startTrackingTimePoint.minus(1); + raceLog.add(new RaceLogDenoteForTrackingEventImpl(denotationTimePoint, + author, raceLog.getCurrentPassId(), trackedRaceName, regatta.getBoatClass(), UUID.randomUUID())); + + raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); + try { - TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); - // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis - // that's why we can't just call adapter.denoteRaceForRaceLogTracking - final TimePoint denotationTimePoint = startTrackingTimePoint.minus(1); - raceLog.add(new RaceLogDenoteForTrackingEventImpl(denotationTimePoint, - author, raceLog.getCurrentPassId(), trackedRaceName, regatta.getBoatClass(), UUID.randomUUID())); - - raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); - final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, - raceColumn, fleet, /* trackWind */true, /* correctWindDirectionByMagneticDeclination */ true); + raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); // TODO do we need to wait or is the TrackedRace guaranteed to be reachable after calling startTracking? raceHandle.getRace(); From 8405f12f68850df2aa401146c22bfc088a2cdd49 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 Jan 2018 16:23:28 +0100 Subject: [PATCH 037/214] Bug 4380: Consistently using the author provided by RacingEventService when adding RaceLog events during expedition all in one import --- .../trackfiles/impl/ExpeditionAllInOneImporter.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 9a0dec2752b..bd66694531d 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -18,7 +18,7 @@ import java.util.logging.Logger; import org.apache.commons.fileupload.FileItem; import org.osgi.framework.BundleContext; -import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; @@ -147,11 +147,6 @@ public class ExpeditionAllInOneImporter { final RankingMetrics rankingMetric = RankingMetrics.ONE_DESIGN; final int[] discardThresholds = new int[0]; - // TODO These are the defaults also used by the UI - final String raceLogEventAuthorName = "Shore"; - final int raceLogEventPriority = 4; - final boolean correctWindDirectionByMagneticDeclination = true; - final ImportResultDTO jsonHolderForGpsFixImport = new ImportResultDTO(logger); final List> filesForGpsFixImport = Arrays.asList(new Pair<>(filenameWithSuffix, fileItem)); try { @@ -227,7 +222,7 @@ public class ExpeditionAllInOneImporter { final Fleet fleet = raceColumn.getFleets().iterator().next(); final RaceLog raceLog = raceColumn.getRaceLog(fleet); - final LogEventAuthorImpl author = new LogEventAuthorImpl(raceLogEventAuthorName, raceLogEventPriority); + final AbstractLogEventAuthor author = service.getServerAuthor(); final TimePoint startOfTracking = firstFixAt; final TimePoint endOfTracking = lastFixAt; @@ -245,8 +240,8 @@ public class ExpeditionAllInOneImporter { raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); - final RaceHandle raceHandle = adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, true, - correctWindDirectionByMagneticDeclination); + final RaceHandle raceHandle = adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, + /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); // TODO do we need to wait or is the TrackedRace guaranteed to be reachable after calling startTracking? raceHandle.getRace(); From 75c5c728409f6245c15bed155e3468df9a11ae1c Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 Jan 2018 16:26:33 +0100 Subject: [PATCH 038/214] Bug 4380: Using most of the original metadata when copying RaceLog events to a sliced race --- .../com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 599db15cf65..88357984319 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7013,14 +7013,17 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { if (raceLogEvent instanceof RaceLogRegisterCompetitorEvent) { final RaceLogRegisterCompetitorEvent raceLogRegisterCompetitorEvent = (RaceLogRegisterCompetitorEvent) raceLogEvent; - raceLog.add(new RaceLogRegisterCompetitorEventImpl(startOfTracking, author, raceLog.getCurrentPassId(), + raceLog.add(new RaceLogRegisterCompetitorEventImpl(raceLogEvent.getLogicalTimePoint(), + raceLogEvent.getAuthor(), raceLog.getCurrentPassId(), raceLogRegisterCompetitorEvent.getCompetitor())); } if (raceLogEvent instanceof RaceLogWindFixEvent) { final RaceLogWindFixEvent raceLogWindFixEvent = (RaceLogWindFixEvent) raceLogEvent; final Wind windFix = raceLogWindFixEvent.getWindFix(); if (timeRange.includes(windFix.getTimePoint())) { - raceLog.add(new RaceLogWindFixEventImpl(windFix.getTimePoint(), author, raceLog.getCurrentPassId(), windFix, raceLogWindFixEvent.isMagnetic())); + raceLog.add( + new RaceLogWindFixEventImpl(raceLogEvent.getLogicalTimePoint(), raceLogEvent.getAuthor(), + raceLog.getCurrentPassId(), windFix, raceLogWindFixEvent.isMagnetic())); } } // TODO do we need to copy more events? From 6c7007abb7697788b5181dc88d0b830c262a6508 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 Jan 2018 16:53:38 +0100 Subject: [PATCH 039/214] Bug 4380: Copying more RaceLog events when slicing a race --- .../gwt/ui/server/SailingServiceImpl.java | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 88357984319..a64bf31c993 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -65,6 +65,7 @@ import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.AllEventsOfTypeFinder; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; @@ -77,6 +78,7 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.LastPublishedCours import com.sap.sailing.domain.abstractlog.race.analyzing.impl.MarkPassingDataFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesEventFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesFinder; +import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEventImpl; @@ -90,10 +92,12 @@ import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.gate.Readon import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.ConfigurableStartModeFlagRacingProcedure; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogRegisterCompetitorEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogUseCompetitorsFromRaceLogEvent; import com.sap.sailing.domain.abstractlog.race.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogDenoteForTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogRegisterCompetitorEventImpl; import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogStartTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.tracking.impl.RaceLogUseCompetitorsFromRaceLogEventImpl; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; @@ -7011,22 +7015,38 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceLog.add(new RaceLogEndOfTrackingEventImpl(endOfTracking, author, raceLog.getCurrentPassId())); for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { - if (raceLogEvent instanceof RaceLogRegisterCompetitorEvent) { - final RaceLogRegisterCompetitorEvent raceLogRegisterCompetitorEvent = (RaceLogRegisterCompetitorEvent) raceLogEvent; - raceLog.add(new RaceLogRegisterCompetitorEventImpl(raceLogEvent.getLogicalTimePoint(), - raceLogEvent.getAuthor(), raceLog.getCurrentPassId(), - raceLogRegisterCompetitorEvent.getCompetitor())); - } - if (raceLogEvent instanceof RaceLogWindFixEvent) { - final RaceLogWindFixEvent raceLogWindFixEvent = (RaceLogWindFixEvent) raceLogEvent; - final Wind windFix = raceLogWindFixEvent.getWindFix(); - if (timeRange.includes(windFix.getTimePoint())) { - raceLog.add( - new RaceLogWindFixEventImpl(raceLogEvent.getLogicalTimePoint(), raceLogEvent.getAuthor(), - raceLog.getCurrentPassId(), windFix, raceLogWindFixEvent.isMagnetic())); + raceLogEvent.accept(new BaseRaceLogEventVisitor() { + @Override + public void visit(RaceLogRegisterCompetitorEvent event) { + raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getCompetitor())); } - } - // TODO do we need to copy more events? + + @Override + public void visit(RaceLogWindFixEvent event) { + final Wind windFix = event.getWindFix(); + if (timeRange.includes(windFix.getTimePoint())) { + raceLog.add(new RaceLogWindFixEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), windFix, event.isMagnetic())); + } + } + + @Override + public void visit(RaceLogUseCompetitorsFromRaceLogEvent event) { + raceLog.add(new RaceLogUseCompetitorsFromRaceLogEventImpl(event.getCreatedAt(), event.getAuthor(), + event.getLogicalTimePoint(), UUID.randomUUID(), raceLog.getCurrentPassId())); + } + + @Override + public void visit(RaceLogCourseDesignChangedEvent event) { + if (!event.getLogicalTimePoint().after(endOfTracking)) { + raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getLogicalTimePoint(), + event.getAuthor(), raceLog.getCurrentPassId(), event.getCourseDesign(), + event.getCourseDesignerMode())); + } + } + // TODO do we need to copy more events? + }); } final TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); From 351b67503f55032f74e9ba30361eea21cef6dec4 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 12 Jan 2018 11:42:38 +0100 Subject: [PATCH 040/214] Bug 4380: Slicing is only available for smartphone-tracked races --- .../sailing/gwt/ui/server/SailingServiceImpl.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index a64bf31c993..77e4309fce3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -6965,7 +6965,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); final Leaderboard regattaLeaderboard = getService().getLeaderboardByName(raceIdentifier.getRegattaName()); final DynamicTrackedRace trackedRace = getService().getTrackedRace(raceIdentifier); - if (regatta == null || !(regattaLeaderboard instanceof RegattaLeaderboard) || trackedRace == null || trackedRace.getStartOfTracking() == null) { + if (regatta == null || !(regattaLeaderboard instanceof RegattaLeaderboard) || trackedRace == null + || trackedRace.getStartOfTracking() == null || !isSmartphoneTrackingEnabled(trackedRace)) { return false; } @@ -6977,6 +6978,17 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return true; } + private boolean isSmartphoneTrackingEnabled(DynamicTrackedRace trackedRace) { + // TODO not sure if this check is sufficient + for (RaceLog raceLog : trackedRace.getAttachedRaceLogs()) { + RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze(); + if (raceLogTrackingState.isForTracking()) { + return true; + } + } + return false; + } + @Override public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimeRange timeRange) { From 2a336dd336a026f06f170480f42b99d3c44dbbd8 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 13 Jan 2018 11:54:12 +0100 Subject: [PATCH 041/214] Changed form params to query params regarding user creation. --- configuration/aws-automation/lib/functions_app.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 79fbd01c3be..3ef9bc8f3cd 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -47,8 +47,7 @@ function change_admin_password(){ # ----------------------------------------------------------- function create_new_user(){ local_echo "Creating new user \"$4\" with password \"$5\"..." - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/create_user" \ - --data "username=$4" --data "password=$5" + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/create_user?username=$4&password=$5" } # ----------------------------------------------------------- From f198d1b91e379db740550f002245d81774a690c2 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 13 Jan 2018 13:40:13 +0100 Subject: [PATCH 042/214] Custom build version input. Made alb domain region specific. --- configuration/aws-automation/aws-setup.sh | 4 +++- configuration/aws-automation/lib/config_eu_west_1.sh | 1 + configuration/aws-automation/lib/config_eu_west_2.sh | 1 + configuration/aws-automation/lib/functions_ec2.sh | 2 +- configuration/aws-automation/lib/functions_elb.sh | 2 +- configuration/aws-automation/lib/functions_io.sh | 5 +++++ configuration/aws-automation/lib/scenario_instance.sh | 1 + .../aws-automation/lib/scenario_sub_instance.sh | 11 ++++++----- configuration/aws-automation/lib/variables.sh | 1 + 9 files changed, 20 insertions(+), 8 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index cbdda789a67..6806c0bcf2e 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -113,7 +113,8 @@ usage() { -l, --instance-short-name Short name for instance (e.g. subdomain \"wcs17\") -a, --new-admin-password New password for the admin user -p, --public-dns-name Dns name of instance (e.g. \"ec2-35-176...amazonaws.com\") - -p, --super-instance Dns name of superior instance (e.g. base instance for sub instances) + -z, --super-instance Dns name of superior instance (e.g. base instance for sub instances) + -b, --build Build version to use (leave empty for latest) -w, --description Description of sub instance -c, --contact-person Contact person -e, --contact-email Email of contact person @@ -220,6 +221,7 @@ while [[ $1 = -?* ]]; do -a|--new-admin-password) shift; new_admin_password_param=${1} ;; -p|--public-dns-name) shift; public_dns_name_param=${1} ;; -b|--super-instance) shift; super_instance_param=${1} ;; + -z|--build) shift; build_version_param=${1} ;; -w|--description) shift; description_param=${1} ;; -c|--contact-person) shift; contact_person_param=${1} ;; -e|--contact-email) shift; contact_email_param=${1} ;; diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index cec3d646977..68ea4e08548 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -13,3 +13,4 @@ certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-47 elb_security_group_ids= mongodb_host=54.76.64.42 mongodb_port=27017 +alb_domain='sapsailing.com' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 0faa1402a4c..5fa74508b78 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -13,3 +13,4 @@ certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b elb_security_group_ids=sg-871732ee mongodb_host=35.176.143.232 mongodb_port=27017 +alb_domain='dummy2.sapsailing.com' diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 861d7641ed4..26c8d3a8ed0 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -26,7 +26,7 @@ function get_default_vpc_id(){ # ----------------------------------------------------------- function get_instance_id(){ local_echo "Querying for the instance id..." - aws_wrapper aws ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text + aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 42e8b285ddf..7ba056f67c9 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -59,7 +59,7 @@ function create_rule(){ local_echo "Creating rule for listener..." local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.sapsailing.com" + local domain="$subdomain.$alb_domain" aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3 1>&2 echo $domain diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 743fc0526c8..085bd348b88 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -120,6 +120,10 @@ function require_contact_email(){ require_variable "$contact_email_param" contact_email "" "$contact_email_message" } +function require_build_version(){ + require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" +} + region_ask_message="Please enter the region for the instance: " instance_type_ask_message="Please enter the instance type: " @@ -138,3 +142,4 @@ super_instance_message="Please enter the dns name of the superior instance: " description_message="Please enter a description for the server: " contact_person_message="Please enter the name of a contact person: " contact_email_message="Please enter the email of the contact person: " +build_version_message="Please enter a build version to use (releases.sapsailng.com): " diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index a72e08be444..c7c1d43fd2b 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -18,6 +18,7 @@ function instance_require(){ require_instance_type require_instance_name require_instance_short_name + require_build_version require_key_name require_key_file require_new_admin_password diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index 765dcf45a75..a47b2eb78f3 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -20,6 +20,7 @@ function sub_instance_require(){ require_key_file require_instance_name require_instance_short_name + require_build_version require_new_admin_password require_user_username require_user_password @@ -30,8 +31,9 @@ function sub_instance_require(){ } function sub_instance_execute() { - local servers_dir="/home/sailing/servers/" - local server_dir="$servers_dir$instance_short_name" + local sailing_dir="/home/sailing" + local servers_dir="$sailing_dir/servers" + local server_dir="$servers_dir/$instance_short_name" local server_env_file="$server_dir/env.sh" local comment_out_line_in_env_with_pattern='JAVA_HOME=\/opt\/jdk1.8.0_20' local comment_in_line_in_env_with_pattern='sapjvm_gc' @@ -40,11 +42,10 @@ function sub_instance_execute() { execute_remote mkdir $server_dir # copy refreshInstance.sh from /servers/server to sub instance directory - execute_remote cp /home/sailing/servers/server/refreshInstance.sh $server_dir + execute_remote cp /home/sailing/code/java/target/refreshInstance.sh $server_dir # Execute refreshInstance.sh - local latest_release=$(get_latest_release) - execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $latest_release;" + execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version;" # uncommenting lines containing pattern execute_remote "sed -i '/$comment_in_line_in_env_with_pattern/s/^#//g' $server_env_file" diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 9d2a64ad2f1..4d9f243b179 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -25,6 +25,7 @@ default_server_startup_notify=leon.radeck@sap.com default_new_admin_password=admin default_user_username=newuser default_user_password=newpassword +latest_release=$(get_latest_release) hosted_zone_id=Z1R8UBAEXAMPLE # Other Variables From b9c4aacc0a0eeba1cfa0559219c2489d1477e71e Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 13 Jan 2018 13:45:21 +0100 Subject: [PATCH 043/214] Default super instance for region eu-west-1 --- configuration/aws-automation/lib/config_eu_west_1.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 68ea4e08548..77c40d56699 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -14,3 +14,6 @@ elb_security_group_ids= mongodb_host=54.76.64.42 mongodb_port=27017 alb_domain='sapsailing.com' + +#SL Multi-Instance Sailing Server +super_instance='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' From 2d8217574403ab27068e3fab2d03abdccc0f295a Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 13 Jan 2018 14:01:29 +0100 Subject: [PATCH 044/214] Fixed typo. Default super instance for region eu-west-2. Appending patch to README file. --- .../aws-automation/lib/config_eu_west_2.sh | 2 ++ configuration/aws-automation/lib/functions_io.sh | 2 +- .../aws-automation/lib/scenario_sub_instance.sh | 14 +++++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 5fa74508b78..758b78ef2e9 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -14,3 +14,5 @@ elb_security_group_ids=sg-871732ee mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' + +super_instance='ec2-35-177-132-223.eu-west-2.compute.amazonaws.com' diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 085bd348b88..96377b45181 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -142,4 +142,4 @@ super_instance_message="Please enter the dns name of the superior instance: " description_message="Please enter a description for the server: " contact_person_message="Please enter the name of a contact person: " contact_email_message="Please enter the email of the contact person: " -build_version_message="Please enter a build version to use (releases.sapsailng.com): " +build_version_message="Please enter a build version to use (releases.sapsailing.com): " diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index a47b2eb78f3..0f022d06516 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -25,9 +25,9 @@ function sub_instance_require(){ require_user_username require_user_password - # require_description - # require_contact_person - # require_contact_email + require_contact_person + require_contact_email + require_description } function sub_instance_execute() { @@ -35,6 +35,7 @@ function sub_instance_execute() { local servers_dir="$sailing_dir/servers" local server_dir="$servers_dir/$instance_short_name" local server_env_file="$server_dir/env.sh" + local readme_file="$servers_dir/README" local comment_out_line_in_env_with_pattern='JAVA_HOME=\/opt\/jdk1.8.0_20' local comment_in_line_in_env_with_pattern='sapjvm_gc' @@ -59,12 +60,15 @@ function sub_instance_execute() { # TODO: Append description to README - local patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ + local env_patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$mongodb_host" \ "MONGODB_PORT=$mongodb_port" "DEPLOY_TO=$instance_short_name") # append patch to env.sh - execute_remote "echo -e \"$patch\" >> $server_env_file" + execute_remote "echo -e \"$env_patch\" >> $server_env_file" + + exceute_remote touch $readme_file + execute_remote "echo -e \"\n# $instance_short_name ($description, $contact_person, $contact_email)\n$env_patch\" >> $readme_file" # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. execute_remote -f "sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" From 7c4d71ec0693afd1b4365a2bc87944275aca9784 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 14 Jan 2018 13:58:53 +0100 Subject: [PATCH 045/214] Enhanced information display, error checking. --- .../aws-automation/lib/functions_app.sh | 13 ++--- .../aws-automation/lib/functions_ec2.sh | 32 +++++++---- .../aws-automation/lib/functions_elb.sh | 3 +- .../aws-automation/lib/functions_io.sh | 2 +- .../aws-automation/lib/functions_wrapper.sh | 31 +++++++---- .../aws-automation/lib/scenario_instance.sh | 18 ++++-- .../lib/scenario_sub_instance.sh | 55 ++++++++++++++----- configuration/aws-automation/lib/variables.sh | 3 + 8 files changed, 103 insertions(+), 54 deletions(-) diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 3ef9bc8f3cd..bbcce40c074 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -58,7 +58,6 @@ function create_new_user(){ # @param $4 port # ----------------------------------------------------------- function wait_for_access_token_resource(){ - local_echo -n "Wait until resource \"/security/api/restsecurity/access_token\" is available..." curl_until_http_200 http://$1:$2@$3:$4/security/api/restsecurity/access_token } @@ -71,8 +70,8 @@ function wait_for_access_token_resource(){ # @return access token # ----------------------------------------------------------- function get_access_token(){ - local_echo "Getting access token..." - wait_for_access_token_resource $1 $2 $3 $4 1>&2 + local_echo -n "Get access token for user $1..." + wait_for_access_token_resource $1 $2 $3 $4 curl_wrapper -X GET "http://$1:$2@$3:$4/security/api/restsecurity/access_token" | get_attribute '.access_token' } @@ -83,7 +82,6 @@ function get_access_token(){ # @param $2 port # ----------------------------------------------------------- function wait_for_create_event_resource(){ - local_echo -n 'Wait until resource "/sailingserver/api/v1/events/createEvent" is available...' curl_until_http_401 http://$1:$2/sailingserver/api/v1/events/createEvent } @@ -96,8 +94,8 @@ function wait_for_create_event_resource(){ # @return event_id of created event # ----------------------------------------------------------- function create_event(){ - local_echo "Creating event with name $4..." - wait_for_create_event_resource $2 $3 1>&2 + local_echo -n "Creating event with name $4..." + wait_for_create_event_resource $2 $3 curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/sailingserver/api/v1/events/createEvent" \ --data "eventName=$4" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' } @@ -111,6 +109,7 @@ function create_event(){ # @param $5 port # ----------------------------------------------------------- function append_event_ssl_macro_to_001_events_conf(){ + local_echo wait_for_ssh_connection $3 $4 wait_for_001_events_patch $3 $4 ssh_wrapper $3@$4 "echo -e \"Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5\" >> $events_conf" @@ -121,10 +120,8 @@ function append_event_ssl_macro_to_001_events_conf(){ # Waits till 001-events.conf exists on the server # ----------------------------------------------------------- function wait_for_001_events_patch(){ - echo -n "Waiting for 001-events.conf to be created..." while [[ $(ssh_wrapper $1@$2 test -f $events_conf && echo "ok") != "ok" ]]; do echo -n . sleep 2 done - success "001-events.conf now exists. Appending macros..." } diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 26c8d3a8ed0..31f50983f52 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -6,7 +6,6 @@ # @return public dns name # ----------------------------------------------------------- function query_public_dns_name(){ - local_echo "Querying for the instance public dns name..." aws_wrapper ec2 describe-instances --instance-ids $1 --output text --query 'Reservations[*].Instances[*].PublicDnsName' } @@ -61,8 +60,7 @@ function get_availability_zones(){ # @param $1 instance_id # ----------------------------------------------------------- function wait_instance_exists(){ - local_echo "Wait until instance \"$1\" is recognized by AWS..." - aws_wrapper ec2 wait instance-exists --instance-ids $1 + aws_wrapper ec2 wait instance-exists --instance-ids $1 &> /dev/null } # ----------------------------------------------------------- @@ -71,23 +69,37 @@ function wait_instance_exists(){ # @param $2 dns name of instance # ----------------------------------------------------------- function wait_for_ssh_connection(){ - echo -n "Connecting to $1@$2..." + local_echo -n "Connecting to $1@$2..." while ! ssh_wrapper -q $1@$2 true 1>&2; do echo -n . sleep 2 done - success "SSH Connection \"$1@$2\" is established." + echo "" + success "[ SSH Connection established ]" } function run_instance(){ - local_echo "Creating instance..." - local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ - "INSTALL_FROM_RELEASE=$(get_latest_release)" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") + "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ + local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n$user_data\n" + + json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ --security-group-ids $instance_security_group_ids --user-data "$user_data" \ - --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } + + local instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') + + echo $instance_id +} + +# ----------------------------------------------------------- +# Get user data of instance with specific instance id +# @param $1 instance id +# @return user data of instance +# ----------------------------------------------------------- +function get_user_data_from_instance() { + aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize | base64 --decode } diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 7ba056f67c9..b029460b868 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -6,7 +6,7 @@ # @return json result # ----------------------------------------------------------- function create_target_group(){ - local_echo "Creating target group..." + local_echo "Creating target group $1..." local target_group_name=$(alphanumeric $1) local vpc_id=$(get_default_vpc_id) aws_wrapper elbv2 create-target-group --name $target_group_name --protocol $target_group_protocol \ @@ -62,7 +62,6 @@ function create_rule(){ local domain="$subdomain.$alb_domain" aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3 1>&2 - echo $domain } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 96377b45181..4a34d959660 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -47,7 +47,7 @@ function require_variable(){ # @param $1 message # ----------------------------------------------------------- function local_echo(){ - echo "$1" >&2 + echo "$@" >&2 } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 7838d76df25..9ca672fac92 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -18,11 +18,11 @@ function curl_wrapper(){ local message=$(get_http_code_message $status_code) if is_http_ok $status_code; then - success "[ OK ]" + success ${response:-"[ OK ]"} echo $response else error "[ ERROR ]" - notice "Function (${FUNCNAME[1]}): [$status_code] $message\n\n$command" + notice "Function (${FUNCNAME[1]}): [$status_code] $message $response\n\n$command" return 1 fi } @@ -35,22 +35,31 @@ function aws_wrapper(){ local out; out=$(aws --region $region "$@") if command_was_successful $?; then - success "[ OK ]" + success ${out:-"[ OK ]"} echo $out | sanitize else - error "[ ERROR ]" + error "[ ERROR ] $out" return 1 fi } + # ----------------------------------------------------------- # Automatically uses key file for ssh if possible. # ----------------------------------------------------------- function ssh_wrapper(){ if [ -z $key_file ]; then - ssh -o StrictHostKeyChecking=no "$@" + out=$(ssh -o StrictHostKeyChecking=no "$@") else - ssh -o StrictHostKeyChecking=no -i $key_file "$@" + out=$(ssh -o StrictHostKeyChecking=no -i $key_file "$@") + fi + + if command_was_successful $?; then + success ${out:-"[ OK ]"} + echo $out | sanitize + else + error "[ ERROR ] $out" + return 1 fi } @@ -60,11 +69,10 @@ function ssh_wrapper(){ function curl_until_http_200(){ while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "200" ]]; do - echo -n "." + local_echo -n "." sleep $http_retry_interval; done - echo "" - success "[ OK ]" + local_echo "" } # ----------------------------------------------------------- @@ -74,9 +82,8 @@ function curl_until_http_200(){ function curl_until_http_401(){ while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "401" ]]; do - echo -n "." + local_echo -n "." sleep $http_retry_interval; done - echo "" - success "[ OK ]" + local_echo "" } diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index c7c1d43fd2b..867fefa2013 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -30,8 +30,7 @@ function instance_execute() { header "Instance Initialization" # create instance - local json_instance=$(run_instance) - instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') + instance_id=$(run_instance) # wait till instance is recognized by aws wait_instance_exists $instance_id @@ -39,6 +38,14 @@ function instance_execute() { # get public dns name of instance public_dns_name=$(query_public_dns_name $instance_id) + description=$(aws ec2 describe-instances --instance-ids $instance_id \ + --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ + --output table) + + if [ command_was_successful $0 ]; then + success $description + fi + wait_for_ssh_connection $ssh_user $public_dns_name # if --tail option is passed then tail logfiles of instance within tmux @@ -46,12 +53,11 @@ function instance_execute() { tail_start fi - header "Event and user creation" - local port="8888" + port="8888" access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) event_id=$(create_event $access_token $public_dns_name $port $instance_name) - change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password - create_new_user $access_token $public_dns_name $port $user_username $user_password + response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) + user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) } diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index 0f022d06516..3fc378eca29 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -5,6 +5,7 @@ function sub_instance_start(){ sub_instance_require + sub_instance_check_preconditions sub_instance_execute } @@ -30,33 +31,54 @@ function sub_instance_require(){ require_description } +function sub_instance_check_preconditions(){ + sailing_dir="/home/sailing" + servers_dir="$sailing_dir/servers" + server_dir="$servers_dir/$instance_short_name" + + header "Checking preconditions" + + echo "Checking if directory $server_dir exists already..." + execute_remote "[ ! -d $server_dir ]" + + echo "Checking if ssh connection $ssh_user@$super_instance is working..." + execute_remote ls +} + function sub_instance_execute() { - local sailing_dir="/home/sailing" - local servers_dir="$sailing_dir/servers" - local server_dir="$servers_dir/$instance_short_name" local server_env_file="$server_dir/env.sh" local readme_file="$servers_dir/README" local comment_out_line_in_env_with_pattern='JAVA_HOME=\/opt\/jdk1.8.0_20' local comment_in_line_in_env_with_pattern='sapjvm_gc' + local refreshInstance_file='/home/sailing/code/java/target/refreshInstance.sh' + + header "Sub instance setup" # create sub instance server directory + local_echo "Creating directory $server_dir ..." execute_remote mkdir $server_dir # copy refreshInstance.sh from /servers/server to sub instance directory - execute_remote cp /home/sailing/code/java/target/refreshInstance.sh $server_dir + local_echo "Copying $refreshInstance_file to $server_dir..." + execute_remote cp $refreshInstance_file $server_dir # Execute refreshInstance.sh - execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version;" + local_echo "Executing refreshInstance.sh with build version $build_version ..." + execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" # uncommenting lines containing pattern + local_echo "Commenting in $comment_in_line_in_env_with_pattern with $server_env_file..." execute_remote "sed -i '/$comment_in_line_in_env_with_pattern/s/^#//g' $server_env_file" # commenting out lines containing pattern + local_echo "Commenting out $comment_out_line_in_env_with_pattern with $server_env_file..." execute_remote "sed -i '/$comment_out_line_in_env_with_pattern/s/^/#/g' $server_env_file" - local server_port=$(find_first_unused_number_for_variable "SERVER_PORT" "8880") - local telnet_port=$(find_first_unused_number_for_variable "TELNET_PORT" "14900") - local expedition_port=$(find_first_unused_number_for_variable "EXPEDITION_PORT" "2000") + local_echo "Searching for free server, telnet and expedition ports..." + local server_port=$(find_first_unused_number_for_variable "SERVER_PORT" $default_server_port) + local telnet_port=$(find_first_unused_number_for_variable "TELNET_PORT" $default_telnet_port) + local expedition_port=$(find_first_unused_number_for_variable "EXPEDITION_PORT" $default_expedition_port) + # TODO: Append description to README @@ -67,34 +89,37 @@ function sub_instance_execute() { # append patch to env.sh execute_remote "echo -e \"$env_patch\" >> $server_env_file" - exceute_remote touch $readme_file + # append patch to README file + execute_remote touch $readme_file execute_remote "echo -e \"\n# $instance_short_name ($description, $contact_person, $contact_email)\n$env_patch\" >> $readme_file" # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. execute_remote -f "sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" + header "Event and user creation" + # get access token access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) # create event - event_id=$(create_event $access_token $super_instance $server_port $instance_name) + event=$(create_event $access_token $super_instance $server_port $instance_name) # change admin password change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password # create new user - create_new_user $access_token $super_instance $server_port $user_username $user_password + user=$(create_new_user $access_token $super_instance $server_port $user_username $user_password) - # TODO: handle situation if super instance is not inside application load balancer + header "Configuring ALB" local target_group_arn=$(create_target_group $instance_name) register_targets $target_group_arn $(get_instance_id $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) - append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + header "Configuring Apache" - # TODO: write email + append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port } # ----------------------------------------------------------- @@ -117,5 +142,5 @@ function execute_remote(){ } function find_all_distinct_values_of_variable_inside_env_files(){ - execute_remote grep -Roh --include=env.sh "$1=.*" $servers_dir | tr -d "$1=" | sort | uniq + execute_remote "grep -Roh --include=env.sh "$1=.*" $servers_dir | tr -d "$1=" | sort | uniq } diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 4d9f243b179..7483e4b1cdb 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -46,3 +46,6 @@ sailing_0='/home/sailing/servers/server/logs/sailing0.log.0' sailing_err='/var/log/sailing.err' sailing_out='/var/log/sailing.out' events_conf='/etc/httpd/conf.d/001-events.conf' +default_server_port='8880' +default_telnet_port='14900' +default_expedition_port='2000' From 70e32582a78acd23f5268bfaf0a96880f7877998 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 14 Jan 2018 14:00:09 +0100 Subject: [PATCH 046/214] Added log message --- configuration/aws-automation/lib/functions_app.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index bbcce40c074..8155c45fbb0 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -109,7 +109,7 @@ function create_event(){ # @param $5 port # ----------------------------------------------------------- function append_event_ssl_macro_to_001_events_conf(){ - local_echo + local_echo "Appending Event-SSL macro to apache config..." wait_for_ssh_connection $3 $4 wait_for_001_events_patch $3 $4 ssh_wrapper $3@$4 "echo -e \"Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5\" >> $events_conf" From ea4c8064926ccaaaa1125b4d7ea3791f6bf018e4 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 14 Jan 2018 17:42:00 +0100 Subject: [PATCH 047/214] Enhanced output. --- .../aws-automation/lib/functions_app.sh | 101 +++++++----------- .../aws-automation/lib/functions_ec2.sh | 8 +- .../aws-automation/lib/functions_elb.sh | 12 +-- .../aws-automation/lib/functions_io.sh | 5 +- .../aws-automation/lib/functions_wrapper.sh | 16 +-- .../aws-automation/lib/scenario_instance.sh | 3 +- .../lib/scenario_sub_instance.sh | 28 +++-- configuration/aws-automation/lib/variables.sh | 2 + 8 files changed, 79 insertions(+), 96 deletions(-) diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 8155c45fbb0..77ce8b011e6 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -22,19 +22,45 @@ function get_latest_release(){ echo "$result" | head -1 } +# ----------------------------------------------------------- +# Get access token +# @param $1 admin username +# @param $2 admin password +# @param $3 dns name of instance +# @param $4 port +# @return access token +# ----------------------------------------------------------- +function get_access_token(){ + local_echo -n "Getting access token..." + curl_until_http_200 http://$1:$2@$3:$4/security/api/restsecurity/access_token + curl_wrapper -X GET "http://$1:$2@$3:$4/security/api/restsecurity/access_token" | get_attribute '.access_token' +} + +# ----------------------------------------------------------- +# Creates a new event with event name = instance name, no regatta and venuename="Default" +# @param $1 access token of privileged user +# @param $2 dns name of instance +# @param $3 port +# @param $4 event name +# @return event_id of created event +# ----------------------------------------------------------- +function create_event(){ + local_echo -n "Creating event with name $4..." + curl_until_http_401 http://$2:$3/sailingserver/api/v1/events/createEvent + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/sailingserver/api/v1/events/createEvent" --data "eventName=$4" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' +} # ----------------------------------------------------------- # Changes password of user # @param $1 access token of privileged user # @param $2 dns name of instance -# @param $3 dns name of instance +# @param $3 port # @param $4 admin username # @param $5 admin new password # ----------------------------------------------------------- function change_admin_password(){ local_echo "Changing password of user $4 to $5..." - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/change_password" \ - --data "username=$4" --data "password=$5" + curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/change_password" --data "username=$4" --data "password=$5" } # ----------------------------------------------------------- @@ -51,76 +77,27 @@ function create_new_user(){ } # ----------------------------------------------------------- -# Wait until access token resource is available -# @param $1 admin username -# @param $2 admin password -# @param $3 public_dns_name -# @param $4 port -# ----------------------------------------------------------- -function wait_for_access_token_resource(){ - curl_until_http_200 http://$1:$2@$3:$4/security/api/restsecurity/access_token -} - -# ----------------------------------------------------------- -# Get access token -# @param $1 admin username -# @param $2 admin password -# @param $3 dns name of instance -# @param $4 port -# @return access token -# ----------------------------------------------------------- -function get_access_token(){ - local_echo -n "Get access token for user $1..." - wait_for_access_token_resource $1 $2 $3 $4 - curl_wrapper -X GET "http://$1:$2@$3:$4/security/api/restsecurity/access_token" | get_attribute '.access_token' -} - - -# ----------------------------------------------------------- -# Wait until create event resource is available -# @param $1 public_dns_name -# @param $2 port -# ----------------------------------------------------------- -function wait_for_create_event_resource(){ - curl_until_http_401 http://$1:$2/sailingserver/api/v1/events/createEvent -} - -# ----------------------------------------------------------- -# Creates a new event with event name = instance name, no regatta and venuename="Default" -# @param $1 access token of privileged user -# @param $2 dns name of instance -# @param $3 port -# @param $4 event name -# @return event_id of created event -# ----------------------------------------------------------- -function create_event(){ - local_echo -n "Creating event with name $4..." - wait_for_create_event_resource $2 $3 - curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/sailingserver/api/v1/events/createEvent" \ - --data "eventName=$4" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' -} - -# ----------------------------------------------------------- -# Patch 001-events.conf with Even-SSL macro +# Patch 001-events.conf with ssl macros # @param $1 dns name # @param $2 event id # @param $3 ssh user # @param $4 public dns name -# @param $5 port +# @param $5 server port # ----------------------------------------------------------- function append_event_ssl_macro_to_001_events_conf(){ - local_echo "Appending Event-SSL macro to apache config..." wait_for_ssh_connection $3 $4 wait_for_001_events_patch $3 $4 - ssh_wrapper $3@$4 "echo -e \"Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5\" >> $events_conf" - ssh_wrapper $3@$4 "/etc/init.d/httpd reload" + local patched_content="Use Event-SSL $1 \"$2\" 127.0.0.1 $5" + local_echo "Appending´\"$patched_content\" to $events_conf..." + ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" + local_echo "Reloading httpd..." + out=$(ssh_wrapper $3@$4 "/etc/init.d/httpd reload") } -# ----------------------------------------------------------- -# Waits till 001-events.conf exists on the server -# ----------------------------------------------------------- function wait_for_001_events_patch(){ - while [[ $(ssh_wrapper $1@$2 test -f $events_conf && echo "ok") != "ok" ]]; do + echo "Waiting for 001-events.conf to be created..." + until ssh_wrapper $1@$2 test -f $events_conf + do echo -n . sleep 2 done diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 31f50983f52..a6e546a9e6c 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -14,7 +14,6 @@ function query_public_dns_name(){ # @return default vpc id # ----------------------------------------------------------- function get_default_vpc_id(){ - local_echo "Querying for the default vpc id..." aws_wrapper ec2 describe-vpcs --query 'Vpcs[?IsDefault==`true`].VpcId' --output text } @@ -24,7 +23,7 @@ function get_default_vpc_id(){ # @return instance id # ----------------------------------------------------------- function get_instance_id(){ - local_echo "Querying for the instance id..." + local_echo "Querying for the instance id of $1..." aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text } @@ -70,15 +69,12 @@ function wait_instance_exists(){ # ----------------------------------------------------------- function wait_for_ssh_connection(){ local_echo -n "Connecting to $1@$2..." - while ! ssh_wrapper -q $1@$2 true 1>&2; do + while ! ssh_prewrapper -q $1@$2 true 1>&2; do echo -n . sleep 2 done - echo "" - success "[ SSH Connection established ]" } - function run_instance(){ local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index b029460b868..770fac79a32 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -45,7 +45,7 @@ function set_target_group_tag(){ # ----------------------------------------------------------- function register_targets(){ local_echo "Register instance within target group..." - aws elbv2 register-targets --target-group-arn $1 --targets Id=$2 + aws_wrapper elbv2 register-targets --target-group-arn $1 --targets Id=$2 } # ----------------------------------------------------------- @@ -60,8 +60,9 @@ function create_rule(){ local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) local domain="$subdomain.$alb_domain" - aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ - --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3 1>&2 + rule=$(aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ + --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3) + echo $domain } # ----------------------------------------------------------- @@ -70,11 +71,10 @@ function create_rule(){ # @return highest priority # ----------------------------------------------------------- function get_rule_with_highest_priority(){ - local max_priority=$(aws_wrapper elbv2 describe-rules --listener-arn $1 \ - --query "sort_by(Rules, &Priority)[*].{P: Priority} | [-2:-1]" --output text) + local max_priority=$(aws elbv2 describe-rules --listener-arn $1 --query "Rules[*].{P:Priority}" --output text | sanitize | sort -n | tail -1) if is_number $max_priority; then echo $max_priority else - echo 1 + echo 0 fi } diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 4a34d959660..f02cad803c5 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -113,18 +113,17 @@ function require_description(){ } function require_contact_person(){ - require_variable "$contact_person_param" contact_person "" "$contact_person_message" + require_variable "$contact_person_param" contact_person "$default_contact_person" "$contact_person_message" } function require_contact_email(){ - require_variable "$contact_email_param" contact_email "" "$contact_email_message" + require_variable "$contact_email_param" contact_email "$default_contact_email" "$contact_email_message" } function require_build_version(){ require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" } - region_ask_message="Please enter the region for the instance: " instance_type_ask_message="Please enter the instance type: " key_name_ask_message="Please enter the name of your keypair to connect to the instance: " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 9ca672fac92..bacebff4c78 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -43,20 +43,22 @@ function aws_wrapper(){ fi } +function ssh_prewrapper(){ + if [ -z $key_file ]; then + ssh -o StrictHostKeyChecking=no "$@" + else + ssh -o StrictHostKeyChecking=no -i $key_file "$@" + fi +} # ----------------------------------------------------------- # Automatically uses key file for ssh if possible. # ----------------------------------------------------------- function ssh_wrapper(){ - if [ -z $key_file ]; then - out=$(ssh -o StrictHostKeyChecking=no "$@") - else - out=$(ssh -o StrictHostKeyChecking=no -i $key_file "$@") - fi - + out=$(ssh_prewrapper "$@") if command_was_successful $?; then success ${out:-"[ OK ]"} - echo $out | sanitize + echo $out else error "[ ERROR ] $out" return 1 diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 867fefa2013..c2589f12461 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -42,7 +42,7 @@ function instance_execute() { --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ --output table) - if [ command_was_successful $0 ]; then + if command_was_successful $?; then success $description fi @@ -54,7 +54,6 @@ function instance_execute() { fi header "Event and user creation" - port="8888" access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) event_id=$(create_event $access_token $public_dns_name $port $instance_name) diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index 3fc378eca29..c57aa630621 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -67,33 +67,37 @@ function sub_instance_execute() { execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" # uncommenting lines containing pattern - local_echo "Commenting in $comment_in_line_in_env_with_pattern with $server_env_file..." + local_echo "Commenting in $comment_in_line_in_env_with_pattern inside $server_env_file..." execute_remote "sed -i '/$comment_in_line_in_env_with_pattern/s/^#//g' $server_env_file" # commenting out lines containing pattern - local_echo "Commenting out $comment_out_line_in_env_with_pattern with $server_env_file..." + local_echo "Commenting out $comment_out_line_in_env_with_pattern inside $server_env_file..." execute_remote "sed -i '/$comment_out_line_in_env_with_pattern/s/^/#/g' $server_env_file" - local_echo "Searching for free server, telnet and expedition ports..." + local_echo "Getting next unused SERVER_PORT..." local server_port=$(find_first_unused_number_for_variable "SERVER_PORT" $default_server_port) + + local_echo "Getting next unused TELNET_PORT..." local telnet_port=$(find_first_unused_number_for_variable "TELNET_PORT" $default_telnet_port) + + local_echo "Getting next unused EXPEDITION_PORT..." local expedition_port=$(find_first_unused_number_for_variable "EXPEDITION_PORT" $default_expedition_port) - - # TODO: Append description to README - local env_patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$mongodb_host" \ "MONGODB_PORT=$mongodb_port" "DEPLOY_TO=$instance_short_name") - # append patch to env.sh + local_echo "Configuring $server_env_file..." execute_remote "echo -e \"$env_patch\" >> $server_env_file" - # append patch to README file + local_echo "Checking for existance of README file..." execute_remote touch $readme_file + + local_echo "Appending patch to README file..." execute_remote "echo -e \"\n# $instance_short_name ($description, $contact_person, $contact_email)\n$env_patch\" >> $readme_file" # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. + local_echo "Starting server..." execute_remote -f "sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" header "Event and user creation" @@ -102,7 +106,7 @@ function sub_instance_execute() { access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) # create event - event=$(create_event $access_token $super_instance $server_port $instance_name) + event_id=$(create_event $access_token $super_instance $server_port $instance_name) # change admin password change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password @@ -120,6 +124,10 @@ function sub_instance_execute() { header "Configuring Apache" append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + + header "Conclusion" + + success "Sub instance should be reachable through https://$domain." } # ----------------------------------------------------------- @@ -142,5 +150,5 @@ function execute_remote(){ } function find_all_distinct_values_of_variable_inside_env_files(){ - execute_remote "grep -Roh --include=env.sh "$1=.*" $servers_dir | tr -d "$1=" | sort | uniq + execute_remote "for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" } diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 7483e4b1cdb..221983df08d 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -49,3 +49,5 @@ events_conf='/etc/httpd/conf.d/001-events.conf' default_server_port='8880' default_telnet_port='14900' default_expedition_port='2000' +default_contact_person='Unknown' +default_contact_email='Unkown' From 16e65c58737c3648f804f07280daf75393a69f79 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 15 Jan 2018 11:49:05 +0100 Subject: [PATCH 048/214] Bug 4380: TimeRange seems to not work as paramter in SailingService --- .../com/sap/sailing/gwt/ui/client/SailingService.java | 3 +-- .../sap/sailing/gwt/ui/client/SailingServiceAsync.java | 3 +-- .../sap/sailing/gwt/ui/server/SailingServiceImpl.java | 10 ++++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 9fb961a7910..28350af4c49 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -105,7 +105,6 @@ import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TimePoint; -import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; @@ -713,5 +712,5 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier); - RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimeRange timeRange); + RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index 1f316ca19ab..ac0cde12aa9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -95,7 +95,6 @@ import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; -import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; @@ -931,6 +930,6 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void canSliceRace(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); - void sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimeRange timeRange, + void sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo, AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 77e4309fce3..799b6f54381 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -6991,7 +6991,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, - TimeRange timeRange) { + TimePoint sliceFrom, TimePoint sliceTo) { if (!canSliceRace(raceIdentifier)) { throw new RuntimeException("Can not slice race"); } @@ -7001,7 +7001,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final DynamicTrackedRace trackedRaceToSlice = getService().getTrackedRace(raceIdentifier); final TimePoint startOfTrackingOfRaceToSlice = trackedRaceToSlice.getStartOfTracking(); final TimePoint endOfTrackingOfRaceToSlice = trackedRaceToSlice.getEndOfTracking(); - if (startOfTrackingOfRaceToSlice.after(timeRange.from()) || (endOfTrackingOfRaceToSlice != null && endOfTrackingOfRaceToSlice.before(timeRange.to()))) { + if (sliceFrom == null || sliceTo == null || startOfTrackingOfRaceToSlice.after(sliceFrom) + || (endOfTrackingOfRaceToSlice != null && endOfTrackingOfRaceToSlice.before(sliceTo))) { throw new RuntimeException("The TimeRange to slice is not part of the race"); } @@ -7021,11 +7022,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final AbstractLogEventAuthor author = getService().getServerAuthor(); - final TimePoint startOfTracking = timeRange.from(); - final TimePoint endOfTracking = timeRange.to(); + final TimePoint startOfTracking = sliceFrom; + final TimePoint endOfTracking = sliceTo; raceLog.add(new RaceLogStartOfTrackingEventImpl(startOfTracking, author, raceLog.getCurrentPassId())); raceLog.add(new RaceLogEndOfTrackingEventImpl(endOfTracking, author, raceLog.getCurrentPassId())); + final TimeRange timeRange = new TimeRangeImpl(sliceFrom, sliceTo); for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { raceLogEvent.accept(new BaseRaceLogEventVisitor() { @Override From 548db19084fb189a631b3e045d32315bb59159fb Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Mon, 15 Jan 2018 16:26:49 +0100 Subject: [PATCH 049/214] Changed target group names to prefix S-dedicated- and S-shared- --- configuration/aws-automation/lib/scenario_associate_alb.sh | 4 ++-- configuration/aws-automation/lib/scenario_sub_instance.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index 9ac4186dab0..3d4722295ea 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -15,12 +15,12 @@ function associate_alb_start(){ # ----------------------------------------------------------- function associate_alb_require(){ # will only be executed after instance creation - : + : } function associate_alb_execute() { # create target group for instance - local target_group_arn=$(create_target_group $instance_name) + local target_group_arn=$(create_target_group "S-dedicated-$instance_name") # add instance to target group register_targets $target_group_arn $instance_id diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index c57aa630621..b0472e93bd5 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -116,7 +116,7 @@ function sub_instance_execute() { header "Configuring ALB" - local target_group_arn=$(create_target_group $instance_name) + local target_group_arn=$(create_target_group "S-shared-$instance_name") register_targets $target_group_arn $(get_instance_id $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) From dd9abeb4c29558b6cc7189350c089c6f3f1aa2ab Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 16 Jan 2018 15:40:49 +0100 Subject: [PATCH 050/214] Bug 4380: Added unfinished test case for race slicing --- .../sap/sailing/gwt/ui/test/SlicingTest.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java new file mode 100644 index 00000000000..12211daa36b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java @@ -0,0 +1,148 @@ +package com.sap.sailing.gwt.ui.test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.Mockito; + +import com.sap.sailing.domain.common.BoatClassMasterdata; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.RankingMetrics; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.abstractlog.TimePointSpecificationFoundInLogImpl; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.common.dto.TrackedRaceDTO; +import com.sap.sailing.gwt.ui.adminconsole.RaceLogSetTrackingTimesDTO; +import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.impl.CountryCodeFactoryImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.util.ClearStateTestSupport; + +import junit.framework.Assert; + +public class SlicingTest { + private final String regattaName = "test"; + private final String boatClassName = BoatClassMasterdata.TP52.name(); + private final TimePoint startOriginalRace = new MillisecondsTimePoint(0); + private final TimePoint endOriginalRace = new MillisecondsTimePoint(300); + private final TimePoint startSlicedRace = new MillisecondsTimePoint(100); + private final TimePoint endSlicedRace = new MillisecondsTimePoint(200); + private final String seriesName = LeaderboardNameConstants.DEFAULT_SERIES_NAME; + private final String fleetName = "default"; + private final String columnNameOriginalRace = "original"; + private final String columnNameSlicedRace = "sliced"; + private final CompetitorDescriptor competitor = new CompetitorDescriptor(null, regattaName, boatClassName, + columnNameOriginalRace, fleetName, "test1", "test", "test", "test", + CountryCodeFactoryImpl.INSTANCE.getFromTwoLetterISOName("de"), null, 1.0, null); + + private SailingServiceImplMock sailingService; + + private ThreadState threadState; + protected Subject subject; + + @Before + public void setUp() throws Exception { + sailingService = new SailingServiceImplMock(); + ((ClearStateTestSupport)(sailingService.getRacingEventService())).clearState(); + + subject = Mockito.mock(Subject.class); + threadState = new SubjectThreadState(subject); + threadState.bind(); + } + + @After + public void tearDown() throws Exception { + threadState.clear(); + ((ClearStateTestSupport)(sailingService.getRacingEventService())).clearState(); + } + + @Test + @Ignore + /** + * Currently non functional test case for race slicing. Should be finished when SailingServiceImplMock ist fixed to + * work properly for test cases. + */ + public void testSliceRace() throws Exception { + final LinkedHashMap seriesCreationParameters = new LinkedHashMap<>(); + final List fleets = new ArrayList<>(); + fleets.add(new FleetDTO(fleetName, 0, null)); + seriesCreationParameters.put(seriesName, + new SeriesCreationParametersDTO(fleets, false, false, true, false, new int[0], false, 0)); + final RegattaCreationParametersDTO regattaCreationParameters = new RegattaCreationParametersDTO( + seriesCreationParameters); + final RegattaDTO regatta = sailingService.createRegatta(regattaName, boatClassName, null, null, + regattaCreationParameters, false, ScoringSchemeType.HIGH_POINT, null, 3.0, false, false, + RankingMetrics.ONE_DESIGN); + final List> columnNames = new ArrayList<>(); + columnNames.add(new Pair<>(columnNameOriginalRace, 0)); + sailingService.addRaceColumnsToSeries(regatta.getRegattaIdentifier(), seriesName, columnNames); + + sailingService.createRegattaLeaderboard(regatta.getRegattaIdentifier(), regattaName, new int[0]); + + final List competitors = sailingService.addCompetitors(Arrays.asList(competitor), null); + final CompetitorDTO competitorDTO = competitors.iterator().next(); + sailingService.setCompetitorRegistrationsInRegattaLog(regattaName, new HashSet<>(competitors)); + + DeviceIdentifierDTO deviceId = new DeviceIdentifierDTO("FILE", UUID.randomUUID().toString()); + final DeviceMappingDTO deviceMapping = new DeviceMappingDTO(deviceId, startOriginalRace.asDate(), endOriginalRace.asDate(), competitorDTO, null); + sailingService.addDeviceMappingToRegattaLog(regattaName, deviceMapping); + + final List> leaderboardRaceColumnFleetNames = new ArrayList<>(); + leaderboardRaceColumnFleetNames.add(new Triple<>(regattaName, columnNameOriginalRace, fleetName)); + + RaceLogSetTrackingTimesDTO raceLogSetTrackingTimes = new RaceLogSetTrackingTimesDTO(); + raceLogSetTrackingTimes.leaderboardName = regattaName; + raceLogSetTrackingTimes.authorName = "Test"; + raceLogSetTrackingTimes.raceColumnName = columnNameOriginalRace; + raceLogSetTrackingTimes.fleetName = fleetName; + raceLogSetTrackingTimes.newStartOfTracking = new TimePointSpecificationFoundInLogImpl(startOriginalRace); + raceLogSetTrackingTimes.newEndOfTracking = new TimePointSpecificationFoundInLogImpl(endOriginalRace); + sailingService.setTrackingTimes(raceLogSetTrackingTimes); + + sailingService.startRaceLogTracking(leaderboardRaceColumnFleetNames , true, false); + // TODO add wind fixes + + StrippedLeaderboardDTO leaderboard = sailingService.getLeaderboard(regattaName); + final RaceColumnDTO columnOfOriginalRace = leaderboard.getRaceList().iterator().next(); + final RaceDTO originalRace = columnOfOriginalRace.getRace(columnOfOriginalRace.getFleets().iterator().next()); + final RegattaAndRaceIdentifier raceIdentifierOfOriginalRace = originalRace.getRaceIdentifier(); + + final RegattaAndRaceIdentifier sliceRaceIdentifier = sailingService.sliceRace(raceIdentifierOfOriginalRace, columnNameSlicedRace, startSlicedRace, endSlicedRace); + + + leaderboard = sailingService.getLeaderboard(regattaName); + final RaceColumnDTO columnOfSlicedRace = leaderboard.getRaceColumnByName(columnNameSlicedRace); + final RaceDTO slicedRace = columnOfSlicedRace.getRace(columnOfOriginalRace.getFleets().iterator().next()); + + Assert.assertEquals(slicedRace.getRaceIdentifier(), sliceRaceIdentifier); + + final TrackedRaceDTO trackedRace = slicedRace.trackedRace; + Assert.assertEquals(startSlicedRace.asDate(), trackedRace.startOfTracking); + Assert.assertEquals(endSlicedRace.asDate(), trackedRace.endOfTracking); + } + +} From f6c1ad45d3d924e8d7497569d2e4ad9a097454bc Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 16 Jan 2018 16:39:34 +0100 Subject: [PATCH 051/214] Minor adjustments. --- .../aws-automation/lib/config_eu_west_1.sh | 2 +- .../aws-automation/lib/config_eu_west_2.sh | 2 +- .../aws-automation/lib/functions_app.sh | 4 +-- .../aws-automation/lib/functions_io.sh | 2 +- .../lib/scenario_associate_alb.sh | 2 +- .../lib/scenario_sub_instance.sh | 5 ++- .../aws-automation/lib/util_functions.sh | 1 + configuration/aws-automation/lib/utils.sh | 2 +- configuration/aws-automation/lib/variables.sh | 34 ++++++++----------- 9 files changed, 26 insertions(+), 28 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 77c40d56699..71d843e737c 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -16,4 +16,4 @@ mongodb_port=27017 alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server -super_instance='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' +default_super_instance='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 758b78ef2e9..8f608f51cfb 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -15,4 +15,4 @@ mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' -super_instance='ec2-35-177-132-223.eu-west-2.compute.amazonaws.com' +default_super_instance='ec2-35-177-132-223.eu-west-2.compute.amazonaws.com' diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 77ce8b011e6..84ad48bfc71 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -87,11 +87,9 @@ function create_new_user(){ function append_event_ssl_macro_to_001_events_conf(){ wait_for_ssh_connection $3 $4 wait_for_001_events_patch $3 $4 - local patched_content="Use Event-SSL $1 \"$2\" 127.0.0.1 $5" + local patched_content="Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5" local_echo "Appending´\"$patched_content\" to $events_conf..." ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" - local_echo "Reloading httpd..." - out=$(ssh_wrapper $3@$4 "/etc/init.d/httpd reload") } function wait_for_001_events_patch(){ diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index f02cad803c5..4e1ef9d9f4c 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -105,7 +105,7 @@ function require_ssh_user(){ } function require_super_instance(){ - require_variable "$super_instance_param" super_instance "" "$super_instance_message" + require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" } function require_description(){ diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index 3d4722295ea..dcf775a3b75 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -20,7 +20,7 @@ function associate_alb_require(){ function associate_alb_execute() { # create target group for instance - local target_group_arn=$(create_target_group "S-dedicated-$instance_name") + local target_group_arn=$(create_target_group "S-dedicated-$instance_short_name") # add instance to target group register_targets $target_group_arn $instance_id diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index b0472e93bd5..4fe21c5f3e5 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -116,7 +116,7 @@ function sub_instance_execute() { header "Configuring ALB" - local target_group_arn=$(create_target_group "S-shared-$instance_name") + local target_group_arn=$(create_target_group "S-shared-$instance_short_name") register_targets $target_group_arn $(get_instance_id $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) @@ -125,6 +125,9 @@ function sub_instance_execute() { append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + local_echo "Reloading httpd..." + out=$(execute_remote "/etc/init.d/httpd reload") + header "Conclusion" success "Sub instance should be reachable through https://$domain." diff --git a/configuration/aws-automation/lib/util_functions.sh b/configuration/aws-automation/lib/util_functions.sh index 99c736a547a..24f39f189b8 100644 --- a/configuration/aws-automation/lib/util_functions.sh +++ b/configuration/aws-automation/lib/util_functions.sh @@ -92,6 +92,7 @@ function alphanumeric(){ lower_trim $1 | only_letters_and_numbers } + # ------------------------------------------------------ # The following functions were part of a bash template # and could be useful in the future diff --git a/configuration/aws-automation/lib/utils.sh b/configuration/aws-automation/lib/utils.sh index 8b9d0533a58..e8fb75bcfac 100644 --- a/configuration/aws-automation/lib/utils.sh +++ b/configuration/aws-automation/lib/utils.sh @@ -87,7 +87,7 @@ do continue fi - # Don't source config files for now + # Don't source region config files (desired file already sourced) if [[ "${utility_file}" == *"config_"* ]]; then continue fi diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 221983df08d..1908121a45a 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -1,22 +1,6 @@ #!/usr/bin/env bash -# SCRIPTNAME -scriptName=`basename $0` #Set Script Name variable -scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName - -# TIMESTAMPS -declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 -datestamp=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 -hourstamp=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM -timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 -today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 -longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 -gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT - -# THISHOST -thisHost=$(hostname) - -# Default region unspecific variables +# START EDIT default_region=eu-west-2 default_key_name=leonradeck-keypair default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' @@ -26,9 +10,21 @@ default_new_admin_password=admin default_user_username=newuser default_user_password=newpassword latest_release=$(get_latest_release) -hosted_zone_id=Z1R8UBAEXAMPLE +# END EDIT -# Other Variables +# Other Variables (DONT EDIT) +scriptName=`basename $0` #Set Script Name variable +scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName +declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 +datestamp=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 +hourstamp=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM +timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 +today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 +longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 +gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT +thisHost=$(hostname) + +hosted_zone_id=Z1R8UBAEXAMPLE target_group_protocol='HTTPS' target_group_port='443' ssh_retry_interval=2 From a1d7af5e899890757b54de99c7d57ca9b0c0e986 Mon Sep 17 00:00:00 2001 From: Papick Garcia Taboada Date: Tue, 16 Jan 2018 18:40:17 +0100 Subject: [PATCH 052/214] Initial UI for slicing --- .../charts/AbstractCompetitorRaceChart.java | 7 ++ .../shared/charts/AbstractRaceChart.java | 36 +++++++++- .../shared/charts/ChartCssResources.java | 2 + .../shared/charts/ChartZoomChangedEvent.java | 37 ++++++++++ .../shared/charts/ChartZoomResetEvent.java | 21 ++++++ .../gwt/ui/client/shared/charts/Charts.css | 11 +++ .../charts/MultiCompetitorRaceChart.java | 67 +++++++++++++++++++ 7 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index 2cd05276c5e..f06b9d315af 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -37,6 +37,7 @@ import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.Widget; @@ -111,6 +112,7 @@ public abstract class AbstractCompetitorRaceChart extends A private final SimpleBusyIndicator busyIndicator; private final Button settingsButton; + private final FlowPanel toolbar = new FlowPanel(); protected AbstractRaceChart(Component parent, ComponentContext context, SailingServiceAsync sailingService, RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, @@ -113,7 +117,10 @@ public abstract class AbstractRaceChart extends A settingsButton = createSettingsButton(); settingsButton.setStyleName(chartsCss.settingsButtonStyle()); settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); + GWT.debugger(); add(settingsButton); + toolbar.addStyleName(chartsCss.toolbar()); + add(toolbar); initWidget(rootPanel); getElement().getStyle().setMarginRight(12, Unit.PX); getElement().getStyle().setMarginLeft(12, Unit.PX); @@ -125,6 +132,15 @@ public abstract class AbstractRaceChart extends A */ protected abstract Button createSettingsButton(); + protected void createExtraToolbarElements(FlowPanel panel) { + } + + @Override + protected final void onLoad() { + createExtraToolbarElements(toolbar); + super.onLoad(); + } + /** * Subclasses need to provide a settings button which will be displayed at a useful position in the layout of this * complex panel, e.g., in the top right corner. @@ -145,7 +161,7 @@ public abstract class AbstractRaceChart extends A Widget child = i.next(); if (child == widget) { foundWidget = true; - } else if (child != settingsButton) { + } else if (child != settingsButton && child != toolbar) { i.remove(); } } @@ -178,8 +194,13 @@ public abstract class AbstractRaceChart extends A } //Set a minute as max time zoom just as for chart if (xAxisMax - xAxisMin > MINUTE_IN_MILLIS) { - timeRangeWithZoomProvider.setTimeZoom(new Date(xAxisMin), new Date(xAxisMax), this); + Date rangeStart = new Date(xAxisMin); + Date rangeEnd = new Date(xAxisMax); + timeRangeWithZoomProvider.setTimeZoom(rangeStart, rangeEnd, this); + fireEvent(new ChartZoomChangedEvent(rangeStart, rangeEnd)); + } else { + fireEvent(new ChartZoomResetEvent()); return false; } } catch (Exception e) { @@ -192,12 +213,13 @@ public abstract class AbstractRaceChart extends A // redraw is triggered by the call to onTimeZoomReset() and therefore not necessary again here // after the selection change event, another click event is sent with the mouse position on the "Reset Zoom" button; ignore that ignoreNextClickEvent = true; + fireEvent(new ChartZoomResetEvent()); } }); } return true; } - + protected boolean onClick(ChartClickEvent chartClickEvent) { if (ignoreNextClickEvent) { ignoreNextClickEvent = false; @@ -293,4 +315,12 @@ public abstract class AbstractRaceChart extends A return timestamp == null || (timer.getPlayState() != PlayStates.Playing && timer.getPlayMode() != PlayModes.Live); } + + public HandlerRegistration addChartZoomChangedHandler(ChartZoomChangedEvent.Handler handler) { + return addHandler(handler, ChartZoomChangedEvent.TYPE); + } + + public HandlerRegistration addChartZoomResetHandler(ChartZoomResetEvent.Handler handler) { + return addHandler(handler, ChartZoomResetEvent.TYPE); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java index 347d997be35..a33c6e03454 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java @@ -24,5 +24,7 @@ public interface ChartCssResources extends ClientBundle { String settingsButtonBackgroundImage(); String chartStyle(); + + String toolbar(); } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java new file mode 100644 index 00000000000..8dd83d61861 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java @@ -0,0 +1,37 @@ +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.Date; + +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; + +public class ChartZoomChangedEvent extends GwtEvent{ + public static final Type TYPE = new Type<>(); + private final Date rangeStart; + private final Date rangeEnd; + interface Handler extends EventHandler { + void handleZoomChanged(ChartZoomChangedEvent cze); + } + + public ChartZoomChangedEvent(Date rangeStart, Date rangeEnd) { + this.rangeStart = rangeStart; + this.rangeEnd = rangeEnd; + } + + public Date getRangeStart() { + return rangeStart; + } + + public Date getRangeEnd() { + return rangeEnd; + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + protected void dispatch(Handler handler) { + handler.handleZoomChanged(this); + }; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java new file mode 100644 index 00000000000..2779880508b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java @@ -0,0 +1,21 @@ +package com.sap.sailing.gwt.ui.client.shared.charts; + +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; + +public class ChartZoomResetEvent extends GwtEvent { + public static final Type TYPE = new Type<>(); + + interface Handler extends EventHandler { + void handleZoomReset(ChartZoomResetEvent czre); + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + protected void dispatch(Handler handler) { + handler.handleZoomReset(this); + }; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css index 94ae74e7154..27b28920ae9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css @@ -32,6 +32,17 @@ top: 0px; } +.toolbar { + position: absolute; + right: 2.6em; + height: 2em; + width: 200px; + bottom: 4px; + + text-align: right; + z-index: 1; +} + @sprite .settingsButtonBackgroundImage { gwt-image: "settingsButton"; width: 2em; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 96bda990cf7..d1d7fed7bb2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,11 +1,18 @@ package com.sap.sailing.gwt.ui.client.shared.charts; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; @@ -30,6 +37,12 @@ import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart implements Component { private final MultiCompetitorRaceChartLifecycle lifecycle; + /** + * TODO: i18n button label + */ + private final Button splitButtonUi = new Button("SPLIT"); + + private TimeRange visibleRange; /** * Creates a Chart used for example in the Raceboard to display various additional data. @@ -48,14 +61,68 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChartcheckIfMaySliceSelectedRegattaAndRace(e)); + addChartZoomResetHandler((e)->splitButtonUi.setVisible(false)); + splitButtonUi.addClickHandler((e)->doSlice()); } + private void doSlice() { + if (visibleRange != null) { + sailingService.sliceRace(selectedRaceIdentifier, "Slice", visibleRange.from(), visibleRange.to(), new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + Window.alert("Caught: " + caught.getMessage()); + + } + + @Override + public void onSuccess(RegattaAndRaceIdentifier result) { + // TODO: goto result?!? + + } + }); + } + } + + private void checkIfMaySliceSelectedRegattaAndRace(final ChartZoomChangedEvent e) { + // TODO: we could cache result + visibleRange = null; + sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { + @Override + public void onSuccess(Boolean result) { + if (result == null) { + splitButtonUi.setVisible(false); + } + boolean yesWeCan = result.booleanValue(); + splitButtonUi.setVisible(yesWeCan); + if (yesWeCan) { + visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), new MillisecondsTimePoint(e.getRangeEnd())); + } + } + + @Override + public void onFailure(Throwable caught) { + splitButtonUi.setVisible(false); + } + }); + } + + @Override protected Button createSettingsButton() { Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); return settingsButton; } + @Override + protected void createExtraToolbarElements(FlowPanel panel) { + panel.add(splitButtonUi); + } + + @Override public MultiCompetitorRaceChartSettings getSettings() { return new MultiCompetitorRaceChartSettings(getAbstractSettings(), getSelectedFirstDetailType(), getSelectedSecondDetailType()); From 6504c64eceac46f6aa793c33172dbf259691438f Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 10:25:20 +0100 Subject: [PATCH 053/214] Bug 4380: Code format changes only --- .../gwt/ui/client/shared/charts/AbstractRaceChart.java | 2 +- .../client/shared/charts/MultiCompetitorRaceChart.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) mode change 100755 => 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java mode change 100755 => 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java old mode 100755 new mode 100644 index d2a258f299c..7045397b4ca --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -132,7 +132,7 @@ public abstract class AbstractRaceChart extends A */ protected abstract Button createSettingsButton(); - protected void createExtraToolbarElements(FlowPanel panel) { + protected void createExtraToolbarElements(FlowPanel panel) { } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java old mode 100755 new mode 100644 index d1d7fed7bb2..31bc1f949df --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -69,7 +69,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { @Override @@ -90,7 +90,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { + sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { @Override public void onSuccess(Boolean result) { if (result == null) { @@ -105,9 +105,9 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart Date: Wed, 17 Jan 2018 10:26:47 +0100 Subject: [PATCH 054/214] Bug 4380: Removed breakpoint that should not have been committed --- .../sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index 7045397b4ca..3087e638654 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -13,7 +13,6 @@ import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.DateTimeFormat; @@ -117,7 +116,6 @@ public abstract class AbstractRaceChart extends A settingsButton = createSettingsButton(); settingsButton.setStyleName(chartsCss.settingsButtonStyle()); settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); - GWT.debugger(); add(settingsButton); toolbar.addStyleName(chartsCss.toolbar()); add(toolbar); From 3fbeb9d976ac4bf1e287cdda911ba36ae4d22525 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 10:29:58 +0100 Subject: [PATCH 055/214] Bug 4380: Temporary solution to ask for the name of the sliced race --- .../charts/MultiCompetitorRaceChart.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 31bc1f949df..2519973ea8d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -9,7 +9,6 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; @@ -70,20 +69,22 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { - - @Override - public void onFailure(Throwable caught) { - Window.alert("Caught: " + caught.getMessage()); + String slicedRaceName = Window.prompt("TODO: Race column name", "Slice"); + if (slicedRaceName != null) { + sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), visibleRange.to(), new AsyncCallback() { - } - - @Override - public void onSuccess(RegattaAndRaceIdentifier result) { - // TODO: goto result?!? + @Override + public void onFailure(Throwable caught) { + Window.alert("Caught: " + caught.getMessage()); + } - } - }); + @Override + public void onSuccess(RegattaAndRaceIdentifier result) { + // TODO: goto result?!? + + } + }); + } } } From 16d6204aa08bd6d34569e27be62654af36a93f15 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 10:37:23 +0100 Subject: [PATCH 056/214] Bug 4380: Fixed locking when copying wind for a sliced race --- .../sap/sailing/gwt/ui/server/SailingServiceImpl.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 799b6f54381..2def8a231e3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7083,8 +7083,13 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources(WindSourceType.EXPEDITION)) { final WindTrack windTrackToCopyFrom = trackedRaceToSlice.getOrCreateWindTrack(windSourceToCopy); - for (Wind windToCopy : windTrackToCopyFrom.getFixes(startOfTracking, true, endOfTracking, true)) { - trackedRace.recordWind(windToCopy, windSourceToCopy); + windTrackToCopyFrom.lockForRead(); + try { + for (Wind windToCopy : windTrackToCopyFrom.getFixes(startOfTracking, true, endOfTracking, true)) { + trackedRace.recordWind(windToCopy, windSourceToCopy); + } + } finally { + windTrackToCopyFrom.unlockAfterRead(); } } return trackedRace.getRaceIdentifier(); From 299f3481e5a89e50b266dc1f3d76b2ff471ddf2d Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 11:43:25 +0100 Subject: [PATCH 057/214] Bug 4380: Copying start/finished times to a sliced race if they are in the sliced tracking interval --- .../gwt/ui/server/SailingServiceImpl.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 2def8a231e3..ea0544986c6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -74,15 +74,21 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.AbortingFlagFinder; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.FinishedTimeFinder; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.FinishingTimeFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.LastPublishedCourseDesignFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.MarkPassingDataFinder; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinder; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderResult; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesEventFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesFinder; import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogSuppressedMarkPassingsEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; @@ -7028,6 +7034,22 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceLog.add(new RaceLogEndOfTrackingEventImpl(endOfTracking, author, raceLog.getCurrentPassId())); final TimeRange timeRange = new TimeRangeImpl(sliceFrom, sliceTo); + final StartTimeFinderResult startTimeFinderResult = new StartTimeFinder(getService(), raceLogOfRaceToSlice).analyze(); + final TimePoint startTime = startTimeFinderResult.getStartTime(); + if (startTime != null && timeRange.includes(startTime)) { + // we do not support depdendent start times here + raceLog.add(new RaceLogStartTimeEventImpl(startTimeFinderResult.getStartTime(), author, + raceLog.getCurrentPassId(), startTimeFinderResult.getStartTime())); + final TimePoint finishingTime = new FinishingTimeFinder(raceLog).analyze(); + if (finishingTime != null && timeRange.includes(finishingTime)) { + raceLog.add(new RaceLogRaceStatusEventImpl(finishingTime, author, raceLog.getCurrentPassId(), RaceLogRaceStatus.FINISHING)); + final TimePoint finishedTime = new FinishedTimeFinder(raceLog).analyze(); + if (finishedTime != null && timeRange.includes(finishedTime)) { + raceLog.add(new RaceLogRaceStatusEventImpl(finishedTime, author, raceLog.getCurrentPassId(), RaceLogRaceStatus.FINISHED)); + } + } + } + for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { raceLogEvent.accept(new BaseRaceLogEventVisitor() { @Override From 84763400e74ee05caf219f16a94f50fa15358433 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 11:56:49 +0100 Subject: [PATCH 058/214] Bug 4380: Copying more RaceLogEvents when slicing a race --- .../gwt/ui/server/SailingServiceImpl.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index ea0544986c6..b8ae3d88122 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -65,6 +65,7 @@ import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.AllEventsOfTypeFinder; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; @@ -83,9 +84,11 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderRes import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesEventFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesFinder; import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFlagEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; @@ -7081,6 +7084,30 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S event.getCourseDesignerMode())); } } + + @Override + public void visit(RaceLogFlagEvent event) { + if (isLatestPassAndInSlicedTrackingInterval(event)) { + raceLog.add(new RaceLogFlagEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getUpperFlag(), event.getLowerFlag(), + event.isDisplayed())); + } + } + + @Override + public void visit(RaceLogCourseAreaChangedEvent event) { + if (isLatestPassAndInSlicedTrackingInterval(event)) { + raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getCourseAreaId())); + } + } + + private boolean isLatestPassAndInSlicedTrackingInterval(RaceLogEvent event) { + if (event.getPassId() != raceLogOfRaceToSlice.getCurrentPassId()) { + return false; + } + return timeRange.includes(event.getLogicalTimePoint()); + } // TODO do we need to copy more events? }); } From 29d4d0f6bc3e53b00b88611462b4f7ae443e3e9d Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 17 Jan 2018 13:20:34 +0100 Subject: [PATCH 059/214] Bug 4380: Copying more RaceLogEvents when slicing a race --- .../sap/sailing/gwt/ui/server/SailingServiceImpl.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index b8ae3d88122..66902f7c374 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -72,6 +72,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.AbortingFlagFinder; @@ -91,6 +92,7 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEvent import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFlagEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogSuppressedMarkPassingsEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl; @@ -7102,6 +7104,14 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } + @Override + public void visit(RaceLogStartProcedureChangedEvent event) { + if (isLatestPassAndInSlicedTrackingInterval(event)) { + raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getStartProcedureType())); + } + } + private boolean isLatestPassAndInSlicedTrackingInterval(RaceLogEvent event) { if (event.getPassId() != raceLogOfRaceToSlice.getCurrentPassId()) { return false; From 47dd85316049e625fb1933231bf12ead3e4ca82a Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Fri, 19 Jan 2018 14:55:02 +0100 Subject: [PATCH 060/214] Adjusted port searching for case where no folders on super instance exist. --- .../aws-automation/lib/config_eu_west_2.sh | 2 +- .../lib/scenario_sub_instance.sh | 42 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 8f608f51cfb..05ece9b0988 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -15,4 +15,4 @@ mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' -default_super_instance='ec2-35-177-132-223.eu-west-2.compute.amazonaws.com' +default_super_instance='ec2-35-176-58-204.eu-west-2.compute.amazonaws.com' diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_sub_instance.sh index 4fe21c5f3e5..248b1bf3f50 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_sub_instance.sh @@ -58,13 +58,22 @@ function sub_instance_execute() { local_echo "Creating directory $server_dir ..." execute_remote mkdir $server_dir + local_echo "Getting next unused SERVER_PORT..." + local server_port=$(find_first_unused_port "SERVER_PORT" $default_server_port) + + local_echo "Getting next unused TELNET_PORT..." + local telnet_port=$(find_first_unused_port "TELNET_PORT" $default_telnet_port) + + local_echo "Getting next unused EXPEDITION_PORT..." + local expedition_port=$(find_first_unused_port "EXPEDITION_PORT" $default_expedition_port) + # copy refreshInstance.sh from /servers/server to sub instance directory local_echo "Copying $refreshInstance_file to $server_dir..." execute_remote cp $refreshInstance_file $server_dir # Execute refreshInstance.sh local_echo "Executing refreshInstance.sh with build version $build_version ..." - execute_remote "export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" + execute_remote "set -o pipefail;export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" # uncommenting lines containing pattern local_echo "Commenting in $comment_in_line_in_env_with_pattern inside $server_env_file..." @@ -74,15 +83,6 @@ function sub_instance_execute() { local_echo "Commenting out $comment_out_line_in_env_with_pattern inside $server_env_file..." execute_remote "sed -i '/$comment_out_line_in_env_with_pattern/s/^/#/g' $server_env_file" - local_echo "Getting next unused SERVER_PORT..." - local server_port=$(find_first_unused_number_for_variable "SERVER_PORT" $default_server_port) - - local_echo "Getting next unused TELNET_PORT..." - local telnet_port=$(find_first_unused_number_for_variable "TELNET_PORT" $default_telnet_port) - - local_echo "Getting next unused EXPEDITION_PORT..." - local expedition_port=$(find_first_unused_number_for_variable "EXPEDITION_PORT" $default_expedition_port) - local env_patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$mongodb_host" \ "MONGODB_PORT=$mongodb_port" "DEPLOY_TO=$instance_short_name") @@ -90,7 +90,7 @@ function sub_instance_execute() { local_echo "Configuring $server_env_file..." execute_remote "echo -e \"$env_patch\" >> $server_env_file" - local_echo "Checking for existance of README file..." + local_echo "Creating README file if it does not exist already..." execute_remote touch $readme_file local_echo "Appending patch to README file..." @@ -98,7 +98,7 @@ function sub_instance_execute() { # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. local_echo "Starting server..." - execute_remote -f "sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" + execute_remote -f "set -o pipefail;sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" header "Event and user creation" @@ -137,21 +137,23 @@ function sub_instance_execute() { # For a given variable name, find first number that is not already used within all env.sh files inside /home/sailing/servers. # If no number is found, return $2 # @param $1 variable name (e.g. SERVER_PORT) -# @param $2 default number (e.g. 8888) +# @param $2 default number (e.g. 8880) # ----------------------------------------------------------- -function find_first_unused_number_for_variable(){ - numbers=($(find_all_distinct_values_of_variable_inside_env_files "$1")) - if [ ${#numbers[@]} -eq 0 ]; then - echo "$2" +function find_first_unused_port(){ + last_port=($(get_last_used_port "$1")) + if [ $? -ne 0 ]; then + port_to_use=$2 else - find_first_missing_number_in_array "${numbers[@]}" + port_to_use=$(($last_port+1)) fi + success $port_to_use + echo $port_to_use } function execute_remote(){ ssh_wrapper $ssh_user@$super_instance "$@" } -function find_all_distinct_values_of_variable_inside_env_files(){ - execute_remote "for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" +function get_last_used_port(){ + ssh_prewrapper $ssh_user@$super_instance "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" } From 51870dadf4b9556bcc5bf7f3db6ffc630d5a36e7 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Fri, 19 Jan 2018 15:36:19 +0100 Subject: [PATCH 061/214] Changing sub instance server port range from 8880-... to 8888-... --- configuration/aws-automation/lib/variables.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 1908121a45a..7da50cf2078 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -42,7 +42,7 @@ sailing_0='/home/sailing/servers/server/logs/sailing0.log.0' sailing_err='/var/log/sailing.err' sailing_out='/var/log/sailing.out' events_conf='/etc/httpd/conf.d/001-events.conf' -default_server_port='8880' +default_server_port='8888' default_telnet_port='14900' default_expedition_port='2000' default_contact_person='Unknown' From ad0793303e30c1582d1c4ee728b34eddeb2a667b Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 22 Jan 2018 09:49:31 +0100 Subject: [PATCH 062/214] bug1301 Added SailingSerializationDummyResult to ensure serializability of DTOs using dispatch communication (DegreePosition). --- .../SailingSerializationDummyResult.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingSerializationDummyResult.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingSerializationDummyResult.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingSerializationDummyResult.java new file mode 100644 index 00000000000..08374455175 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingSerializationDummyResult.java @@ -0,0 +1,17 @@ +package com.sap.sailing.gwt.home.communication; + +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sse.gwt.dispatch.shared.commands.Result; + +public class SailingSerializationDummyResult implements Result { + + private DegreePosition degreePosition; + + private SailingSerializationDummyResult() { + } + + public DegreePosition getDegreePosition() { + return degreePosition; + } + +} From 7435297144ed36a5188ee4ff712097f68276a3c8 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 22 Jan 2018 09:53:39 +0100 Subject: [PATCH 063/214] bug1301 Avoid potential NPE in wind overlays for Windfinder soruces --- .../sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 25e801270a5..93f3b4cfbd7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -11,6 +11,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import com.google.gwt.core.client.GWT; @@ -2767,12 +2768,13 @@ public class RaceMap extends AbstractCompositeComponent impleme Collection marksToZoom = forMap.windSensorOverlays.values(); if (marksToZoom != null) { for (WindSensorOverlay windSensorOverlay : marksToZoom) { - if (BoundsUtil.getAsPosition(windSensorOverlay.getLatLngPosition()) != null) { - LatLngBounds bounds = BoundsUtil.getAsBounds(windSensorOverlay.getLatLngPosition()); + final LatLng latLngPosition = windSensorOverlay.getLatLngPosition(); + if (Objects.nonNull(latLngPosition) && BoundsUtil.getAsPosition(latLngPosition) != null) { + LatLngBounds bounds = BoundsUtil.getAsBounds(latLngPosition); if (newBounds == null) { newBounds = bounds; } else { - newBounds = newBounds.extend(windSensorOverlay.getLatLngPosition()); + newBounds = newBounds.extend(latLngPosition); } } } From b68c2a18ef9a4c3ff367decdb83ad7cd00a85c2f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Mon, 22 Jan 2018 20:38:45 +0100 Subject: [PATCH 064/214] Removed scenario --tail. --- configuration/aws-automation/aws-setup.sh | 11 ++---- .../aws-automation/lib/functions_tmux.sh | 2 +- .../aws-automation/lib/scenario_instance.sh | 5 --- .../aws-automation/lib/scenario_tail.sh | 36 ------------------- 4 files changed, 3 insertions(+), 51 deletions(-) delete mode 100644 configuration/aws-automation/lib/scenario_tail.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 6806c0bcf2e..8bbdfd01cca 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -127,7 +127,6 @@ usage() { --associate-alb Associate instance with existing application load balancer whos listener is defined in variables_aws.sh. Automatically create necessary target group and host name rule. - --tail Tail logs from instance using tmux ${bold}Other:${reset} --version Output version information and exit @@ -140,12 +139,8 @@ usage() { Associate application load balancer: > ./aws-setup.sh --instance --associate-alb - Associate instance with application load balancer while - automatically tailing important log files (tmux required): - > ./aws-setup.sh --instance --associate-alb --tail - - Tail logfiles of running instance with dns name: - > ./aws-setup.sh --tail --public-dns-name ec2-x.compute.amazonaws.com + Associate instance with application load balancer: + > ./aws-setup.sh --instance --associate-alb Create instance and use default values: > ./aws-setup.sh --instance --force @@ -202,7 +197,6 @@ unset options associate_alb=false instance=false sub_instance=false -tail=false # Read the options and set variables while [[ $1 = -?* ]]; do @@ -230,7 +224,6 @@ while [[ $1 = -?* ]]; do --instance) instance=true ;; --sub-instance) sub_instance=true ;; --associate-alb) associate_alb=true ;; - --tail) tail=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; esac diff --git a/configuration/aws-automation/lib/functions_tmux.sh b/configuration/aws-automation/lib/functions_tmux.sh index dadb804ab8a..4a332509f89 100644 --- a/configuration/aws-automation/lib/functions_tmux.sh +++ b/configuration/aws-automation/lib/functions_tmux.sh @@ -80,7 +80,7 @@ function check_dependencies() { function check_environment() { if ! inside_tmux_session; then - echo "To tail instance log files, please run this script inside a tmux session. To do so enter \"tmux\" into the console and start the script from there." + echo "Please run this script inside a tmux session. To do so enter \"tmux\" into the console and start the script from there." safeExit fi } diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index c2589f12461..b2e4992cedb 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -48,11 +48,6 @@ function instance_execute() { wait_for_ssh_connection $ssh_user $public_dns_name - # if --tail option is passed then tail logfiles of instance within tmux - if $tail; then - tail_start - fi - header "Event and user creation" port="8888" access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) diff --git a/configuration/aws-automation/lib/scenario_tail.sh b/configuration/aws-automation/lib/scenario_tail.sh deleted file mode 100644 index c4d7068a548..00000000000 --- a/configuration/aws-automation/lib/scenario_tail.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -function tail_start(){ - tail_precondition - tail_user_input - tail_execute -} - -function tail_precondition(){ - check_if_tmux_is_used -} - -# ----------------------------------------------------------- -# All these variables are needed for this scenario -# If one variable is not assigned or passed by parameter -# the user will be prompted to enter a value -# ----------------------------------------------------------- -function tail_user_input(){ - require_key_file - require_ssh_user - require_public_dns_name -} - -function tail_execute() { - echo "Open tmux panes and start tailing log files..." - construct_ui - wait_for_ssh_connection "$ssh_user" "$public_dns_name" - open_connections "$ssh_user" "$public_dns_name" - tail_logfiles -} - -function tail_logfiles(){ - tmux send-keys -t 1 "clear;echo \"Waiting for file $sailing_0 to appear...\";touch $sailing_0;cat $sailing_0;tail -F -v $sailing_0" C-m - tmux send-keys -t 2 "clear;cat $sailing_out;tail -F -v $sailing_out" C-m - tmux send-keys -t 3 "clear;cat $sailing_err;tail -F -v $sailing_err" C-m -} From e09df3e1918d46d896b3d39e9d07b03fdf211c71 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 23 Jan 2018 13:10:54 +0100 Subject: [PATCH 065/214] Refactoring --- configuration/aws-automation/aws-setup.sh | 10 ---------- .../aws-automation/launch_template.json | 1 + .../aws-automation/lib/functions_app.sh | 9 +++------ .../aws-automation/lib/functions_ec2.sh | 17 ++++------------- .../aws-automation/lib/functions_wrapper.sh | 8 ++++++++ .../aws-automation/lib/scenario_instance.sh | 6 +++++- 6 files changed, 21 insertions(+), 30 deletions(-) create mode 100644 configuration/aws-automation/launch_template.json diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 8bbdfd01cca..921fe4ced34 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -68,10 +68,6 @@ echo -n source_region_config_file -if $tail; then - check_if_tmux_is_used -fi - if $instance; then instance_start if $associate_alb; then @@ -87,12 +83,6 @@ if $sub_instance; then safeExit fi -if $tail ; then - tail_start - confirm_reset_panes - safeExit -fi - } usage() { diff --git a/configuration/aws-automation/launch_template.json b/configuration/aws-automation/launch_template.json new file mode 100644 index 00000000000..2301bce6354 --- /dev/null +++ b/configuration/aws-automation/launch_template.json @@ -0,0 +1 @@ +{"UserData":"HALLO","ImageId":"ami-39f3e25d","InstanceType":"t2.medium","TagSpecifications":[{"ResourceType":"instance","Tags":[{"Key":"Type","Value":"Replica"}]}]} \ No newline at end of file diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 84ad48bfc71..a969496248b 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -85,8 +85,8 @@ function create_new_user(){ # @param $5 server port # ----------------------------------------------------------- function append_event_ssl_macro_to_001_events_conf(){ - wait_for_ssh_connection $3 $4 wait_for_001_events_patch $3 $4 + local patched_content="Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5" local_echo "Appending´\"$patched_content\" to $events_conf..." ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" @@ -94,9 +94,6 @@ function append_event_ssl_macro_to_001_events_conf(){ function wait_for_001_events_patch(){ echo "Waiting for 001-events.conf to be created..." - until ssh_wrapper $1@$2 test -f $events_conf - do - echo -n . - sleep 2 - done + wait_for_ssh_connection $1 $2 + do_until_true ssh_wrapper $1@$2 test -f $events_conf } diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index a6e546a9e6c..24253be8a8e 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -69,26 +69,17 @@ function wait_instance_exists(){ # ----------------------------------------------------------- function wait_for_ssh_connection(){ local_echo -n "Connecting to $1@$2..." - while ! ssh_prewrapper -q $1@$2 true 1>&2; do - echo -n . - sleep 2 - done + do_until_true ssh_prewrapper -q $1@$2 true 1>&2 } function run_instance(){ - local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ - "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ - "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - - local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n$user_data\n" + local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n$1\n" json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ - --security-group-ids $instance_security_group_ids --user-data "$user_data" \ + --security-group-ids $instance_security_group_ids --user-data "$1" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } - local instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') - - echo $instance_id + echo "$json_instance" | get_attribute '.Instances[0].InstanceId' } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index bacebff4c78..565829f659e 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -89,3 +89,11 @@ function curl_until_http_401(){ done local_echo "" } + +function do_until_true(){ + until "$@" + do + echo -n . + sleep $http_retry_interval + done +} diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index b2e4992cedb..ae2e0c37ebd 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -29,8 +29,12 @@ function instance_require(){ function instance_execute() { header "Instance Initialization" + local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ + "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") + # create instance - instance_id=$(run_instance) + instance_id=$(run_instance "$user_data") # wait till instance is recognized by aws wait_instance_exists $instance_id From 2e0d1c71b6a598c469d795572c1b2166baed9309 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 23 Jan 2018 13:37:13 +0100 Subject: [PATCH 066/214] Target group health check. Sub instance changed to shared instance. Corrected target group name format. --- configuration/aws-automation/aws-setup.sh | 10 +++++----- .../aws-automation/lib/functions_elb.sh | 16 +++++++++------- .../lib/scenario_associate_alb.sh | 1 + ...instance.sh => scenario_shared_instance.sh} | 18 ++++++++++-------- .../aws-automation/lib/util_functions.sh | 4 ++++ 5 files changed, 29 insertions(+), 20 deletions(-) rename configuration/aws-automation/lib/{scenario_sub_instance.sh => scenario_shared_instance.sh} (93%) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 921fe4ced34..fc934d2d940 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -77,8 +77,8 @@ if $instance; then safeExit fi -if $sub_instance; then - sub_instance_start +if $shared_instance; then + shared_instance_start confirm_reset_panes safeExit fi @@ -113,7 +113,7 @@ usage() { ${bold}Scenarios:${reset} --instance Create instance - --sub-instance Create sub instance + --shared-instance Create sub instance --associate-alb Associate instance with existing application load balancer whos listener is defined in variables_aws.sh. Automatically create necessary target group and host name rule. @@ -186,7 +186,7 @@ unset options # Set default value of variable without parameter value to false associate_alb=false instance=false -sub_instance=false +shared_instance=false # Read the options and set variables while [[ $1 = -?* ]]; do @@ -212,7 +212,7 @@ while [[ $1 = -?* ]]; do -f|--force) force=true ;; -d|--debug) debug=true ;; --instance) instance=true ;; - --sub-instance) sub_instance=true ;; + --shared-instance) shared_instance=true ;; --associate-alb) associate_alb=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 770fac79a32..14ffd7f0393 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -7,7 +7,7 @@ # ----------------------------------------------------------- function create_target_group(){ local_echo "Creating target group $1..." - local target_group_name=$(alphanumeric $1) + local target_group_name=$(only_letters_numbers_dash $1) local vpc_id=$(get_default_vpc_id) aws_wrapper elbv2 create-target-group --name $target_group_name --protocol $target_group_protocol \ --port $target_group_port --vpc-id $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn' @@ -18,14 +18,16 @@ function create_target_group(){ # @param $1 target group arn # @param $2 health check protocol (e.g. "HTTP") # @param $3 health check path (e.g. "/index.html") -# @param $4 health check interval seconds (e.g. "5") -# @param $5 health check timeouts seconds (e.g. "4") -# @param $6 healthy threshold count (e.g. "2") -# @param $7 unhealthy threshold count (e.g. "2") +# @param $4 health check port (e.g. "443") +# @param $5 health check interval seconds (e.g. "5") +# @param $6 health check timeouts seconds (e.g. "4") +# @param $7 healthy threshold count (e.g. "2") +# @param $8 unhealthy threshold count (e.g. "2") # ----------------------------------------------------------- function set_target_group_health_check(){ - aws_wrapper elbv2 modify-target-group --target-group-arn $1 --health-check-protocol $2 --health-check-path $3 \ - --health-check-interval-seconds $4 --health-check-timeout-seconds $5 --healthy-threshold-count $6 --unhealthy-threshold-count $7 + local_echo "Setting target group health check (protocol: $2, path: $3, port: $4, interval seconds: $5, timeout seconds: $6, unhealthy-count: $7, healthy-count: $8)..." + aws_wrapper elbv2 modify-target-group --target-group-arn $1 --health-check-protocol $2 --health-check-path $3 --health-check-port $4\ + --health-check-interval-seconds $5 --health-check-timeout-seconds $6 --healthy-threshold-count $7 --unhealthy-threshold-count $8 } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index dcf775a3b75..a3840b6ffb6 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -21,6 +21,7 @@ function associate_alb_require(){ function associate_alb_execute() { # create target group for instance local target_group_arn=$(create_target_group "S-dedicated-$instance_short_name") + set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "80" "5" "4" "2" "2" # add instance to target group register_targets $target_group_arn $instance_id diff --git a/configuration/aws-automation/lib/scenario_sub_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh similarity index 93% rename from configuration/aws-automation/lib/scenario_sub_instance.sh rename to configuration/aws-automation/lib/scenario_shared_instance.sh index 248b1bf3f50..74f3aeec2f5 100644 --- a/configuration/aws-automation/lib/scenario_sub_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Scenario for creating a sub instance +# Scenario for creating a shared instance # ------------------------------------------------------ -function sub_instance_start(){ - sub_instance_require - sub_instance_check_preconditions - sub_instance_execute +function shared_instance_start(){ + shared_instance_require + shared_instance_check_preconditions + shared_instance_execute } # ----------------------------------------------------------- @@ -14,7 +14,7 @@ function sub_instance_start(){ # If one variable is not assigned or passed by parameter # the user will be prompted to enter a value # ----------------------------------------------------------- -function sub_instance_require(){ +function shared_instance_require(){ require_super_instance require_key_name @@ -31,7 +31,7 @@ function sub_instance_require(){ require_description } -function sub_instance_check_preconditions(){ +function shared_instance_check_preconditions(){ sailing_dir="/home/sailing" servers_dir="$sailing_dir/servers" server_dir="$servers_dir/$instance_short_name" @@ -45,7 +45,7 @@ function sub_instance_check_preconditions(){ execute_remote ls } -function sub_instance_execute() { +function shared_instance_execute() { local server_env_file="$server_dir/env.sh" local readme_file="$servers_dir/README" local comment_out_line_in_env_with_pattern='JAVA_HOME=\/opt\/jdk1.8.0_20' @@ -117,6 +117,8 @@ function sub_instance_execute() { header "Configuring ALB" local target_group_arn=$(create_target_group "S-shared-$instance_short_name") + set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "$server_port" "5" "4" "2" "2" + register_targets $target_group_arn $(get_instance_id $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) diff --git a/configuration/aws-automation/lib/util_functions.sh b/configuration/aws-automation/lib/util_functions.sh index 24f39f189b8..300cff00ad6 100644 --- a/configuration/aws-automation/lib/util_functions.sh +++ b/configuration/aws-automation/lib/util_functions.sh @@ -92,6 +92,10 @@ function alphanumeric(){ lower_trim $1 | only_letters_and_numbers } +function only_letters_numbers_dash(){ + echo $1 | trim | tr -d -c '[:alnum:]-' +} + # ------------------------------------------------------ # The following functions were part of a bash template From b60ba25598aa07fc8d0a3dfab12e07a738915183 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 23 Jan 2018 13:43:10 +0100 Subject: [PATCH 067/214] Adjusted eu-west-1 config --- configuration/aws-automation/lib/config_eu_west_1.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 71d843e737c..5573fe08c69 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -6,14 +6,14 @@ listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/S # Sailing Analytics App instance_security_group_ids=sg-eaf31e85 -# SAP Sailing Analytics App HVM 1.59 -image_id='ami-407ef339' +# SAP Sailing Analytics App HVM 1.61 +image_id='ami-7719840e' certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' elb_security_group_ids= -mongodb_host=54.76.64.42 -mongodb_port=27017 +mongodb_host= +mongodb_port= alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server -default_super_instance='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' +default_super_instance='' From 27cb744f17e11b85847a08c79cc7625051cc7c1e Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 23 Jan 2018 14:20:57 +0100 Subject: [PATCH 068/214] Master-instance parameter. Refactoring. --- configuration/aws-automation/aws-setup.sh | 5 ++- .../aws-automation/lib/functions_ec2.sh | 10 ++--- .../aws-automation/lib/scenario_instance.sh | 37 ++++++++++++------- .../lib/scenario_master_instance.sh | 1 + 4 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 configuration/aws-automation/lib/scenario_master_instance.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index fc934d2d940..84140743d08 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -113,7 +113,8 @@ usage() { ${bold}Scenarios:${reset} --instance Create instance - --shared-instance Create sub instance + --shared-instance Create sub instance + --master-instance Create master instance --associate-alb Associate instance with existing application load balancer whos listener is defined in variables_aws.sh. Automatically create necessary target group and host name rule. @@ -187,6 +188,7 @@ unset options associate_alb=false instance=false shared_instance=false +master_instance=false # Read the options and set variables while [[ $1 = -?* ]]; do @@ -213,6 +215,7 @@ while [[ $1 = -?* ]]; do -d|--debug) debug=true ;; --instance) instance=true ;; --shared-instance) shared_instance=true ;; + --master-instance) master_instance=true ;; --associate-alb) associate_alb=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 24253be8a8e..81e9fecf89c 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -64,16 +64,16 @@ function wait_instance_exists(){ # ----------------------------------------------------------- # Wait until ssh connection is established -# @param $1 ssh user -# @param $2 dns name of instance +# @param $1 instance id # ----------------------------------------------------------- function wait_for_ssh_connection(){ - local_echo -n "Connecting to $1@$2..." - do_until_true ssh_prewrapper -q $1@$2 true 1>&2 + public_dns_name=$(query_public_dns_name $1) + local_echo -n "Connecting to $ssh_user@$public_dns_name..." + do_until_true ssh_prewrapper -q $ssh_user@$public_dns_name true 1>&2 } function run_instance(){ - local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n$1\n" + local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ --security-group-ids $instance_security_group_ids --user-data "$1" \ diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index ae2e0c37ebd..af9bcf31d06 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -26,6 +26,10 @@ function instance_require(){ require_user_password } +# ----------------------------------------------------------- +# Execute instance scenario +# @param $1 user data +# ----------------------------------------------------------- function instance_execute() { header "Instance Initialization" @@ -33,15 +37,29 @@ function instance_execute() { "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - # create instance - instance_id=$(run_instance "$user_data") + instance_id=$(create_instance "$user_data") + + wait_for_ssh_connection $instance_id + + header "Event and user creation" + + port="8888" + access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) + event_id=$(create_event $access_token $public_dns_name $port $instance_name) + response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) + user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) +} + +# ----------------------------------------------------------- +# Create instance and output response +# @param $1 user data +# ----------------------------------------------------------- +function create_instance(){ + instance_id=$(run_instance "$1") # wait till instance is recognized by aws wait_instance_exists $instance_id - # get public dns name of instance - public_dns_name=$(query_public_dns_name $instance_id) - description=$(aws ec2 describe-instances --instance-ids $instance_id \ --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ --output table) @@ -50,12 +68,5 @@ function instance_execute() { success $description fi - wait_for_ssh_connection $ssh_user $public_dns_name - - header "Event and user creation" - port="8888" - access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) - event_id=$(create_event $access_token $public_dns_name $port $instance_name) - response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) - user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) + echo $instance_id } diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh new file mode 100644 index 00000000000..f1f641af19b --- /dev/null +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -0,0 +1 @@ +#!/usr/bin/env bash From 1b60d4952a3317d81cef65a8a6396323376c5b33 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 23 Jan 2018 15:48:47 +0100 Subject: [PATCH 069/214] First step into master/replica management. Creating replica launch template right after launching master. Doing this allows to use auto scaling groups in the future. --- configuration/aws-automation/aws-setup.sh | 2 + .../aws-automation/lib/functions_ec2.sh | 21 +++++- .../aws-automation/lib/scenario_instance.sh | 21 ------ .../lib/scenario_master_instance.sh | 64 +++++++++++++++++++ 4 files changed, 85 insertions(+), 23 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 84140743d08..01ace58fbb3 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -67,6 +67,8 @@ function mainScript() { echo -n source_region_config_file +master_instance_start +safeExit if $instance; then instance_start diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 81e9fecf89c..bafb1bdc9c5 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -72,14 +72,31 @@ function wait_for_ssh_connection(){ do_until_true ssh_prewrapper -q $ssh_user@$public_dns_name true 1>&2 } -function run_instance(){ +# ----------------------------------------------------------- +# Create instance and output response +# @param $1 user data +# ----------------------------------------------------------- +function create_instance(){ local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ --security-group-ids $instance_security_group_ids --user-data "$1" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } - echo "$json_instance" | get_attribute '.Instances[0].InstanceId' + instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') + + # wait till instance is recognized by aws + wait_instance_exists $instance_id + + description=$(aws ec2 describe-instances --instance-ids $instance_id \ + --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ + --output table) + + if command_was_successful $?; then + success $description + fi + + echo $instance_id } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index af9bcf31d06..f2c7b5e3cca 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -49,24 +49,3 @@ function instance_execute() { response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) } - -# ----------------------------------------------------------- -# Create instance and output response -# @param $1 user data -# ----------------------------------------------------------- -function create_instance(){ - instance_id=$(run_instance "$1") - - # wait till instance is recognized by aws - wait_instance_exists $instance_id - - description=$(aws ec2 describe-instances --instance-ids $instance_id \ - --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ - --output table) - - if command_was_successful $?; then - success $description - fi - - echo $instance_id -} diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index f1f641af19b..1eaa1c0c44d 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -1 +1,65 @@ #!/usr/bin/env bash + +#!/usr/bin/env bash + +# Scenario for creating an instance +# ------------------------------------------------------ + +function master_instance_start(){ + master_instance_require + master_instance_execute +} + +# ----------------------------------------------------------- +# All these variables are needed for this scenario +# If one variable is not assigned or passed by parameter +# the user will be prompted to enter a value +# ----------------------------------------------------------- +function master_instance_require(){ + require_region + require_instance_type + require_instance_name + require_instance_short_name + require_build_version + require_key_name + require_key_file + require_new_admin_password + require_user_username + require_user_password +} + +# ----------------------------------------------------------- +# Execute instance scenario +# @param $1 user data +# ----------------------------------------------------------- +function master_instance_execute() { + header "Master Instance Initialization" + + local user_data_replica=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-replica-server" \ + "REPLICATE_MASTER_SERVLET_HOST=$public_dns_name" "REPLICATE_MASTER_EXCHANGE_HOST=$(alphanumeric $instance_name)" "EVENT_ID=123" "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") + + local base64_user_data=$(echo "$user_data_replica" | base64 -w 0) + local launch_template_for_replica=$(printf '{"UserData":"%s","ImageId":"%s","InstanceType":"%s","TagSpecifications":[{"ResourceType":"instance","Tags":[{"Key":"Name","Value":"%s"}]}], "SecurityGroupIds": ["%s"], "KeyName": "%s"}'\ + "${base64_user_data}" "$image_id" "$instance_type" "$instance_name (Replica)" "$instance_security_group_ids" "$key_name") + + aws_wrapper ec2 create-launch-template --launch-template-name "Replica_$instance_short_name" --version-description "Replica for $instance_name." --launch-template-data $launch_template_for_replica + safeExit + + local user_data_master=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-master-server" \ + "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") + + instance_id=$(create_instance "$user_data_master") + + wait_for_ssh_connection $instance_id + + header "Event and user creation" + + port="8888" + access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) + event_id=$(create_event $access_token $public_dns_name $port $instance_name) + response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) + user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) + +} From c6fde763366ca48b324e2787bb1faef8b9cd2795 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 26 Jan 2018 00:10:16 +0100 Subject: [PATCH 070/214] made WindFinder connector robust against missing Country ("c") information Change-Id: I5b34228cfd6bd46a684040a64864abeaf2047e0e --- .../impl/WindFinderReportParserTest.java | 9 +++++++++ .../impl/ReviewedSpotsCollectionImpl.java | 2 +- .../windfinderadapter/impl/WindFinderReportParser.java | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java index 4144e563ba8..ccc205ddc77 100755 --- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java +++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParserTest.java @@ -46,6 +46,15 @@ public class WindFinderReportParserTest { assertEquals(331.0, wind.getFrom().getDegrees(), 0.0000001); assertPositionEquals(new DegreePosition(54.47, 10.28), wind.getPosition(), 0.000001); } + + @Test + public void testOtherSpots() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException { + assertEquals(4, Util.size(new ReviewedSpotsCollectionImpl("kielerfoerde").getSpots(/* cached */ false))); + assertEquals(3, Util.size(new ReviewedSpotsCollectionImpl("chiemsee").getSpots(/* cached */ false))); + assertEquals(1, Util.size(new ReviewedSpotsCollectionImpl("starnbergersee").getSpots(/* cached */ false))); + assertEquals(1, Util.size(new ReviewedSpotsCollectionImpl("wannsee").getSpots(/* cached */ false))); + assertEquals(4, Util.size(new ReviewedSpotsCollectionImpl("travemuende").getSpots(/* cached */ false))); + } @Test public void testReadingSpotDescriptions() throws IOException, ParseException { diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java index f1699cb4ff7..df19a0f3ce1 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java @@ -40,6 +40,7 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { public ReviewedSpotsCollectionImpl(String id) { this.id = id; + this.parser = new WindFinderReportParser(); this.spotsByIdCache = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().schedule(()->{ final ConcurrentMap result = new ConcurrentHashMap<>(); for (final Spot spot : loadSpots()) { @@ -47,7 +48,6 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection { } return result; }, /* delay */ 0, TimeUnit.MILLISECONDS); - this.parser = new WindFinderReportParser(); } @Override diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index 79d3e423b44..80b33180f24 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -54,7 +54,7 @@ public class WindFinderReportParser { return new SpotImpl(jsonOfSingleSpot.get("n").toString(), jsonOfSingleSpot.get("id").toString(), jsonOfSingleSpot.get("kw").toString(), - jsonOfSingleSpot.get("c").toString(), + jsonOfSingleSpot.get("c") == null ? null : jsonOfSingleSpot.get("c").toString(), new DegreePosition(((Number) jsonOfSingleSpot.get("lat")).doubleValue(), ((Number) jsonOfSingleSpot.get("lon")).doubleValue()), this, reviewedSpotsCollection); } From 55ab9f0485aa447925eec89a08fc1a8bffdba826 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Fri, 26 Jan 2018 12:10:19 +0100 Subject: [PATCH 071/214] Changed db name of replica. Use default key per specific regions. --- configuration/aws-automation/aws-setup.sh | 9 ++++---- .../aws-automation/lib/config_eu_west_1.sh | 3 +++ .../aws-automation/lib/functions_ec2.sh | 7 +++++- .../lib/scenario_master_instance.sh | 23 ++++++++++--------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 01ace58fbb3..a6b9e5ddd81 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -67,21 +67,22 @@ function mainScript() { echo -n source_region_config_file -master_instance_start -safeExit if $instance; then instance_start if $associate_alb; then associate_alb_start fi - confirm_reset_panes safeExit fi if $shared_instance; then shared_instance_start - confirm_reset_panes + safeExit +fi + +if $master_instance; then + master_instance_start safeExit fi diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 5573fe08c69..7352b62dda8 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -1,5 +1,8 @@ # Variables for region "eu-west-1" (Ireland) +default_key_name=Leon +default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' + # HTTPS Listener of Application Load Balancer Sailing-eu-west-1 listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/Sailing-eu-west-1/32b89dbfe7f75097/f9212223209ac042' diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index bafb1bdc9c5..bd8e98b6975 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -27,6 +27,11 @@ function get_instance_id(){ aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text } +function get_public_ip(){ + local_echo "Querying for the public ip of $1..." + aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicIpAddress=='$1'].InstanceId" --output text +} + # ----------------------------------------------------------- # Allocates an elastic ip address # @return elastic ip @@ -88,7 +93,7 @@ function create_instance(){ # wait till instance is recognized by aws wait_instance_exists $instance_id - description=$(aws ec2 describe-instances --instance-ids $instance_id \ + description=$(aws --region $region ec2 describe-instances --instance-ids $instance_id \ --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ --output table) diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index 1eaa1c0c44d..baddce684d4 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -35,17 +35,6 @@ function master_instance_require(){ function master_instance_execute() { header "Master Instance Initialization" - local user_data_replica=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ - "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-replica-server" \ - "REPLICATE_MASTER_SERVLET_HOST=$public_dns_name" "REPLICATE_MASTER_EXCHANGE_HOST=$(alphanumeric $instance_name)" "EVENT_ID=123" "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - - local base64_user_data=$(echo "$user_data_replica" | base64 -w 0) - local launch_template_for_replica=$(printf '{"UserData":"%s","ImageId":"%s","InstanceType":"%s","TagSpecifications":[{"ResourceType":"instance","Tags":[{"Key":"Name","Value":"%s"}]}], "SecurityGroupIds": ["%s"], "KeyName": "%s"}'\ - "${base64_user_data}" "$image_id" "$instance_type" "$instance_name (Replica)" "$instance_security_group_ids" "$key_name") - - aws_wrapper ec2 create-launch-template --launch-template-name "Replica_$instance_short_name" --version-description "Replica for $instance_name." --launch-template-data $launch_template_for_replica - safeExit - local user_data_master=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-master-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") @@ -62,4 +51,16 @@ function master_instance_execute() { response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) + public_ip=$(get_public_ip $public_dns_name) + + local user_data_replica=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)-replica" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-replica-server" \ + "REPLICATE_MASTER_SERVLET_HOST=$public_dns_name" "REPLICATE_MASTER_EXCHANGE_HOST=$(alphanumeric $instance_name)" "EVENT_ID=$event_id" "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") + + local base64_user_data=$(echo "$user_data_replica" | base64 -w 0) + local launch_template_for_replica=$(printf '{"UserData":"%s","ImageId":"%s","InstanceType":"%s","TagSpecifications":[{"ResourceType":"instance","Tags":[{"Key":"Name","Value":"%s"}]}], "SecurityGroupIds": ["%s"], "KeyName": "%s"}'\ + "${base64_user_data}" "$image_id" "$instance_type" "$instance_name (Replica)" "$instance_security_group_ids" "$key_name") + + aws_wrapper ec2 create-launch-template --launch-template-name "Replica_$instance_short_name" --version-description "Replica for $instance_name." --launch-template-data $launch_template_for_replica + } From 3340da5ef7468c6c0d338dca35af2739624407b1 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 09:13:53 +0100 Subject: [PATCH 072/214] Bug 4380: Implemented proposing a race name when slicing --- .../sailing/gwt/ui/client/SailingService.java | 2 + .../gwt/ui/client/SailingServiceAsync.java | 2 + .../charts/MultiCompetitorRaceChart.java | 40 ++++++++++++------- .../gwt/ui/server/SailingServiceImpl.java | 23 ++++++++++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 28350af4c49..aa7f10402f0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -713,4 +713,6 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier); RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo); + + String proposeSlicedRaceName(RegattaAndRaceIdentifier raceIdentifier); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index ac0cde12aa9..04f0092466a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -932,4 +932,6 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo, AsyncCallback callback); + + void proposeSlicedRaceName(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 2519973ea8d..539d29f8fa9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -69,22 +69,32 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { - - @Override - public void onFailure(Throwable caught) { - Window.alert("Caught: " + caught.getMessage()); + sailingService.proposeSlicedRaceName(selectedRaceIdentifier, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + Window.alert("Caught: " + caught.getMessage()); + } + + @Override + public void onSuccess(String proposedRaceName) { + final String slicedRaceName = Window.prompt("TODO: Race column name", proposedRaceName); + if (slicedRaceName != null) { + sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), visibleRange.to(), new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + Window.alert("Caught: " + caught.getMessage()); + } + + @Override + public void onSuccess(RegattaAndRaceIdentifier result) { + // TODO: goto result?!? + + } + }); } - - @Override - public void onSuccess(RegattaAndRaceIdentifier result) { - // TODO: goto result?!? - - } - }); - } + } + }); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 66902f7c374..1e2cf893646 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7000,6 +7000,28 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return false; } + public String proposeSlicedRaceName(final RegattaAndRaceIdentifier raceIdentifier) { + final Leaderboard regattaLeaderboard = getService().getLeaderboardByName(raceIdentifier.getRegattaName()); + + String prefix = null; + int currentCount = 0; + final Pattern pattern = Pattern.compile("^([a-zA-Z]+)([0-9]+)$"); + for (RaceColumn column : regattaLeaderboard.getRaceColumns()) { + final Matcher matcher = pattern.matcher(column.getName()); + if (matcher.matches()) { + prefix = matcher.group(1); + currentCount = Integer.parseInt(matcher.group(2)); + } + } + + if (prefix == null) { + prefix = "R"; + } + currentCount++; + return prefix + currentCount; + } + + @Override public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo) { @@ -7155,6 +7177,5 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } catch (Exception e) { throw new RuntimeException("Error while slicing race"); } - } } From ae564aa9ce0258b7610e1d669f8acc2dc06e16c1 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 09:14:46 +0100 Subject: [PATCH 073/214] Bug 4380: Added TODO --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 1e2cf893646..83433a91a29 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7028,6 +7028,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S if (!canSliceRace(raceIdentifier)) { throw new RuntimeException("Can not slice race"); } + // TODO check if the race column name is already used final String trackedRaceName = newRaceColumnName; final RegattaIdentifier regattaIdentifier = new RegattaName(raceIdentifier.getRegattaName()); final Regatta regatta = getService().getRegatta(regattaIdentifier); From 5d6b1039093898d501b048a172e184e39665ccc0 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 10:43:27 +0100 Subject: [PATCH 074/214] Bug 4380: Added summary dialog to be shown after slicing a race (consistent to expedition all in one import) --- .../ExpeditionAllInOneImportResultDialog.java | 54 +++-------------- .../charts/AbstractCompetitorRaceChart.java | 4 +- .../charts/MultiCompetitorRaceChart.java | 15 +++-- .../race/TrackedRaceCreationResultDialog.java | 58 +++++++++++++++++++ .../gwt/ui/raceboard/RaceBoardPanel.java | 2 +- 5 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/race/TrackedRaceCreationResultDialog.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionAllInOneImportResultDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionAllInOneImportResultDialog.java index 06ca9877c71..62ff2ffe953 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionAllInOneImportResultDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionAllInOneImportResultDialog.java @@ -1,55 +1,15 @@ package com.sap.sailing.gwt.ui.adminconsole; -import java.util.Collections; import java.util.UUID; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.settings.client.EntryPointWithSettingsLinkFactory; -import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.gwt.client.dialog.DataEntryDialog; +import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; -public class ExpeditionAllInOneImportResultDialog extends DataEntryDialog { +public class ExpeditionAllInOneImportResultDialog extends TrackedRaceCreationResultDialog { - private final VerticalPanel verticalPanel; - - public ExpeditionAllInOneImportResultDialog(UUID eventId, String regattaName, String raceName, - String leaderboardName, String leaderboardGroupName) { - super(StringMessages.INSTANCE.importFinished(), StringMessages.INSTANCE.importFinishedMessage(), - StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), /* validator */ null, - new DialogCallback() { - @Override - public void ok(Void editedObject) { - } - - @Override - public void cancel() { - } - }); - - verticalPanel = new VerticalPanel(); - verticalPanel.setSpacing(20); - Anchor raceboardAnchor = new Anchor(StringMessages.INSTANCE.importFinishedGotoRaceboard(), - EntryPointWithSettingsLinkFactory.createRaceBoardLinkWithDefaultSettings(eventId, leaderboardName, - leaderboardGroupName, regattaName, raceName)); - raceboardAnchor.setTarget("_blank"); - verticalPanel.add(raceboardAnchor); - Anchor eventAnchor = new Anchor(StringMessages.INSTANCE.importFinishedGotoEvent(), - EntryPointLinkFactory.createEventPlaceLink(eventId.toString(), Collections.emptyMap())); - eventAnchor.setTarget("_blank"); - verticalPanel.add(eventAnchor); - - } - - @Override - protected Widget getAdditionalWidget() { - return verticalPanel; - } - - @Override - protected Void getResult() { - return null; - } + public ExpeditionAllInOneImportResultDialog(UUID eventId, String regattaName, String raceName, + String leaderboardName, String leaderboardGroupName) { + super(StringMessages.INSTANCE.importFinished(), StringMessages.INSTANCE.importFinishedMessage(), eventId, + regattaName, raceName, leaderboardName, leaderboardGroupName); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index f06b9d315af..2f4ec0e29eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -94,8 +94,8 @@ public abstract class AbstractCompetitorRaceChart, Series> dataSeriesForDetailTypeAndCompetitor = new HashMap<>(); private final Map, Series> markPassingSeriesByCompetitor = new HashMap<>(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 539d29f8fa9..d7ded9ce141 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.ui.client.shared.charts; +import java.util.UUID; + import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; @@ -9,6 +11,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; import com.sap.sse.common.TimeRange; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; @@ -39,9 +42,10 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChartcheckIfMaySliceSelectedRegattaAndRace(e)); @@ -88,8 +93,10 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart { + + private final VerticalPanel verticalPanel; + + public TrackedRaceCreationResultDialog(String title, String message, UUID eventId, String regattaName, String raceName, + String leaderboardName, String leaderboardGroupName) { + super(title, message, + StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), /* validator */ null, + new DialogCallback() { + @Override + public void ok(Void editedObject) { + } + + @Override + public void cancel() { + } + }); + + verticalPanel = new VerticalPanel(); + verticalPanel.setSpacing(20); + Anchor raceboardAnchor = new Anchor(StringMessages.INSTANCE.importFinishedGotoRaceboard(), + EntryPointWithSettingsLinkFactory.createRaceBoardLinkWithDefaultSettings(eventId, leaderboardName, + leaderboardGroupName, regattaName, raceName)); + raceboardAnchor.setTarget("_blank"); + verticalPanel.add(raceboardAnchor); + + if (eventId != null) { + Anchor eventAnchor = new Anchor(StringMessages.INSTANCE.importFinishedGotoEvent(), + EntryPointLinkFactory.createEventPlaceLink(eventId.toString(), Collections.emptyMap())); + eventAnchor.setTarget("_blank"); + verticalPanel.add(eventAnchor); + } + + } + + @Override + protected Widget getAdditionalWidget() { + return verticalPanel; + } + + @Override + protected Void getResult() { + return null; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 9199ee1ea03..0ac42917835 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -361,7 +361,7 @@ public class RaceBoardPanel sailingService, asyncActionsExecutor, competitorSelectionProvider, selectedRaceIdentifier, timer, timeRangeWithZoomModel, stringMessages, - errorReporter, true, true, leaderboardGroupName, leaderboardName); + errorReporter, true, true, leaderboardGroupName, leaderboardName, eventId); competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); competitorChart.setVisible(false); competitorChart.updateSettings(multiCompetitorRaceChartSettings); From c817086a08c5047e0bf33f946ef0ffa624a0ab36 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 12:43:47 +0100 Subject: [PATCH 075/214] Bug 4380: Code simplification --- .../ui/client/shared/charts/MultiCompetitorRaceChart.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index d7ded9ce141..887dd6ce744 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -111,10 +111,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { @Override public void onSuccess(Boolean result) { - if (result == null) { - splitButtonUi.setVisible(false); - } - boolean yesWeCan = result.booleanValue(); + boolean yesWeCan = Boolean.TRUE.equals(result); splitButtonUi.setVisible(yesWeCan); if (yesWeCan) { visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), new MillisecondsTimePoint(e.getRangeEnd())); From 0e5e1b4034cf2963cf8eb83be1e1aeca1b58a99c Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 15:26:30 +0100 Subject: [PATCH 076/214] Bug 4380: Using a DataEntryDialog including a validator to ask for the name of a sliced race --- .../charts/MultiCompetitorRaceChart.java | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 887dd6ce744..81aee9bcacc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,11 +1,15 @@ package com.sap.sailing.gwt.ui.client.shared.charts; import java.util.UUID; +import java.util.function.Consumer; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; @@ -17,6 +21,7 @@ import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; @@ -82,8 +87,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart { sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), visibleRange.to(), new AsyncCallback() { @Override @@ -93,13 +97,13 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart { + + private final TextBox raceNameInput; + + public SlicedRaceNameDialog(String initialValue, Consumer okCallback) { + super("TODO: Slice race", "TODO: Please enter a name for the sliced race", StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { + @Override + public String getErrorMessage(String valueToValidate) { + if (valueToValidate == null || valueToValidate.isEmpty()) { + return "TODO: the race name must not be empty"; + } + return null; + } + }, new DialogCallback() { + @Override + public void ok(String editedObject) { + okCallback.accept(editedObject); + } + @Override + public void cancel() { + } + }); + raceNameInput = createTextBox(initialValue); + raceNameInput.addAttachHandler(e -> { + if (e.isAttached()) { + Scheduler.get().scheduleDeferred(() -> raceNameInput.setFocus(true)); + } + }); + } + + @Override + protected Widget getAdditionalWidget() { + return raceNameInput; + } + + @Override + protected String getResult() { + return raceNameInput.getValue(); + } + } } From 17167846dcd9b1666b8ed5cc9fd4c1311143c4a9 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 16:08:01 +0100 Subject: [PATCH 077/214] Bug 4380: Implemented validation to check if a race name for a sliced race is already used --- .../sailing/gwt/ui/client/SailingService.java | 3 +- .../gwt/ui/client/SailingServiceAsync.java | 4 ++- .../charts/MultiCompetitorRaceChart.java | 16 ++++++---- .../gwt/ui/server/SailingServiceImpl.java | 11 +++++-- .../ui/shared/SliceRacePreperationDTO.java | 30 +++++++++++++++++++ 5 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index aa7f10402f0..554ee526047 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -88,6 +88,7 @@ import com.sap.sailing.gwt.ui.shared.RemoteSailingServerReferenceDTO; import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; import com.sap.sailing.gwt.ui.shared.ServerConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SimulatorResultsDTO; +import com.sap.sailing.gwt.ui.shared.SliceRacePreperationDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingArchiveConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingConfigurationDTO; @@ -714,5 +715,5 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo); - String proposeSlicedRaceName(RegattaAndRaceIdentifier raceIdentifier); + SliceRacePreperationDTO prepareForSlicingOfRace(RegattaAndRaceIdentifier raceIdentifier); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index 04f0092466a..696c6fe994d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -79,6 +79,7 @@ import com.sap.sailing.gwt.ui.shared.SailingServiceConstants; import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; import com.sap.sailing.gwt.ui.shared.ServerConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SimulatorResultsDTO; +import com.sap.sailing.gwt.ui.shared.SliceRacePreperationDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingArchiveConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingConfigurationDTO; @@ -933,5 +934,6 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo, AsyncCallback callback); - void proposeSlicedRaceName(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); + void prepareForSlicingOfRace(RegattaAndRaceIdentifier raceIdentifier, + AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 81aee9bcacc..e4bf892cf77 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -16,6 +16,7 @@ import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; +import com.sap.sailing.gwt.ui.shared.SliceRacePreperationDTO; import com.sap.sse.common.TimeRange; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; @@ -79,15 +80,15 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { + sailingService.prepareForSlicingOfRace(selectedRaceIdentifier, new AsyncCallback() { @Override public void onFailure(Throwable caught) { Window.alert("Caught: " + caught.getMessage()); } @Override - public void onSuccess(String proposedRaceName) { - new SlicedRaceNameDialog(proposedRaceName, slicedRaceName -> { + public void onSuccess(SliceRacePreperationDTO sliceRacePreperatioData) { + new SlicedRaceNameDialog(sliceRacePreperatioData, slicedRaceName -> { sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), visibleRange.to(), new AsyncCallback() { @Override @@ -205,13 +206,16 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart okCallback) { + public SlicedRaceNameDialog(final SliceRacePreperationDTO sliceRacePreperatioData, final Consumer okCallback) { super("TODO: Slice race", "TODO: Please enter a name for the sliced race", StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { @Override public String getErrorMessage(String valueToValidate) { if (valueToValidate == null || valueToValidate.isEmpty()) { return "TODO: the race name must not be empty"; } + if (sliceRacePreperatioData.getAlreadyUsedNames().contains(valueToValidate)) { + return "The race name is already used in the same leaderboard"; + } return null; } }, new DialogCallback() { @@ -224,7 +228,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart { if (e.isAttached()) { Scheduler.get().scheduleDeferred(() -> raceNameInput.setFocus(true)); @@ -239,7 +243,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart alreadyUsedRaceNames = new HashSet<>(); for (RaceColumn column : regattaLeaderboard.getRaceColumns()) { + alreadyUsedRaceNames.add(column.getName()); final Matcher matcher = pattern.matcher(column.getName()); if (matcher.matches()) { prefix = matcher.group(1); @@ -7018,7 +7022,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S prefix = "R"; } currentCount++; - return prefix + currentCount; + return new SliceRacePreperationDTO(prefix + currentCount, alreadyUsedRaceNames); } @@ -7032,6 +7036,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final String trackedRaceName = newRaceColumnName; final RegattaIdentifier regattaIdentifier = new RegattaName(raceIdentifier.getRegattaName()); final Regatta regatta = getService().getRegatta(regattaIdentifier); + if (regatta.getRaceColumnByName(newRaceColumnName) != null) { + throw new RuntimeException("The race column name is already used in the given regatta"); + } final DynamicTrackedRace trackedRaceToSlice = getService().getTrackedRace(raceIdentifier); final TimePoint startOfTrackingOfRaceToSlice = trackedRaceToSlice.getStartOfTracking(); final TimePoint endOfTrackingOfRaceToSlice = trackedRaceToSlice.getEndOfTracking(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java new file mode 100644 index 00000000000..c6d26e2b138 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java @@ -0,0 +1,30 @@ +package com.sap.sailing.gwt.ui.shared; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.rpc.IsSerializable; +import com.sap.sse.common.Util; + +public class SliceRacePreperationDTO implements IsSerializable { + + private String proposedRaceName; + private HashSet alreadyUsedNames; + + public SliceRacePreperationDTO(String proposedRaceName, Iterable alreadyUsedNames) { + this.proposedRaceName = proposedRaceName; + this.alreadyUsedNames = new HashSet<>(); + Util.addAll(alreadyUsedNames, this.alreadyUsedNames); + } + + protected SliceRacePreperationDTO() { + } + + public String getProposedRaceName() { + return proposedRaceName; + } + + public Set getAlreadyUsedNames() { + return alreadyUsedNames; + } +} From 09766c8f0b1010a212b42af5cff6513d163971b4 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 30 Jan 2018 17:12:04 +0100 Subject: [PATCH 078/214] Bug 4380: I18n --- .../sailing/gwt/ui/client/StringMessages.java | 7 +++++++ .../gwt/ui/client/StringMessages.properties | 7 +++++++ .../ui/client/StringMessages_de.properties | 7 +++++++ .../charts/MultiCompetitorRaceChart.java | 20 ++++++++----------- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 097d301b161..00083d7447a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2050,4 +2050,11 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String chooseAName(); String unexpectedErrorDuringFileImport(); String couldNotDownload(String url); + String sliceRace(); + String slicingARaceWasSuccessful(); + String enterNameForSlicedRace(); + String raceNameIsRequired(); + String raceNameIsAlreadyUsed(); + String errorWhilePreparingToSliceARace(); + String errorWhileSlicingARace(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index c009fff343d..f697b0742b8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2038,3 +2038,10 @@ importFinishedGotoEvent=Open event chooseAName=Choose a name unexpectedErrorDuringFileImport=An unexpected error occurred during file upload. couldNotDownload=Could not download file: {0} +sliceRace=Slice race +slicingARaceWasSuccessful=Slicing a race was successful. +enterNameForSlicedRace=Please enter a name for the race to be sliced +raceNameIsRequired=A valid name must be given for the sliced race +raceNameIsAlreadyUsed=The given name is already used +errorWhilePreparingToSliceARace=Error while preparing to slice a race +errorWhileSlicingARace=Error while slicing a race diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 99d8c6518b4..1befc11f3f4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2035,3 +2035,10 @@ importFinishedGotoEvent=Event öffnen chooseAName=Namen auswählen unexpectedErrorDuringFileImport=Ein unerwarteter Fehler ist beim Datei-Import aufgetreten. couldNotDownload=Konnte Datei nicht laden: {0} +sliceRace=Rennen schneiden +slicingARaceWasSuccessful=Das Schneiden eines Rennens war erfolgreich. +enterNameForSlicedRace=Bitte einen Namen für das neue Rennen eingeben +raceNameIsRequired=Es muss ein gültiger Name für das geschnittene Rennen eingegeben werden +raceNameIsAlreadyUsed=Der Name wird bereits verwendet +errorWhilePreparingToSliceARace=Ein Fehler ist bei den Vorbereitungen für das schneiden eines Rennens aufgetreten +errorWhileSlicingARace=Ein Fehler ist beim Schneiden eines Rennens aufgetreten diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index e4bf892cf77..95162131dc8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -4,7 +4,6 @@ import java.util.UUID; import java.util.function.Consumer; import com.google.gwt.core.client.Scheduler; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; @@ -45,10 +44,7 @@ import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart implements Component { private final MultiCompetitorRaceChartLifecycle lifecycle; - /** - * TODO: i18n button label - */ - private final Button splitButtonUi = new Button("TODO: slice"); + private final Button splitButtonUi = new Button(StringMessages.INSTANCE.sliceRace()); private TimeRange visibleRange; private final UUID eventId; @@ -83,7 +79,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart() { @Override public void onFailure(Throwable caught) { - Window.alert("Caught: " + caught.getMessage()); + errorReporter.reportError(StringMessages.INSTANCE.errorWhilePreparingToSliceARace()); } @Override @@ -93,13 +89,13 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart okCallback) { - super("TODO: Slice race", "TODO: Please enter a name for the sliced race", StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { + super(StringMessages.INSTANCE.sliceRace(), StringMessages.INSTANCE.enterNameForSlicedRace(), StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { @Override public String getErrorMessage(String valueToValidate) { if (valueToValidate == null || valueToValidate.isEmpty()) { - return "TODO: the race name must not be empty"; + return StringMessages.INSTANCE.raceNameIsRequired(); } if (sliceRacePreperatioData.getAlreadyUsedNames().contains(valueToValidate)) { - return "The race name is already used in the same leaderboard"; + return StringMessages.INSTANCE.raceNameIsAlreadyUsed(); } return null; } From aa6ecb0da562288c828417550eab6f90c3dc036e Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Thu, 1 Feb 2018 01:30:47 +0100 Subject: [PATCH 079/214] Wait for ssh now takes ssh_user and dns_name as parameters --- configuration/aws-automation/lib/functions_app.sh | 7 ++++++- configuration/aws-automation/lib/functions_ec2.sh | 8 ++++---- configuration/aws-automation/lib/scenario_instance.sh | 3 ++- .../aws-automation/lib/scenario_master_instance.sh | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index a969496248b..0d353305317 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -78,7 +78,7 @@ function create_new_user(){ # ----------------------------------------------------------- # Patch 001-events.conf with ssl macros -# @param $1 dns name +# @param $1 domain # @param $2 event id # @param $3 ssh user # @param $4 public dns name @@ -92,6 +92,11 @@ function append_event_ssl_macro_to_001_events_conf(){ ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" } +# ----------------------------------------------------------- +# Wait until 001_events.conf file appears on instance +# @param $1 ssh user +# @param $2 dns name +# ----------------------------------------------------------- function wait_for_001_events_patch(){ echo "Waiting for 001-events.conf to be created..." wait_for_ssh_connection $1 $2 diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index bd8e98b6975..c3566c366f6 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -69,12 +69,12 @@ function wait_instance_exists(){ # ----------------------------------------------------------- # Wait until ssh connection is established -# @param $1 instance id +# @param $1 ssh_user +# @param $2 public dns name # ----------------------------------------------------------- function wait_for_ssh_connection(){ - public_dns_name=$(query_public_dns_name $1) - local_echo -n "Connecting to $ssh_user@$public_dns_name..." - do_until_true ssh_prewrapper -q $ssh_user@$public_dns_name true 1>&2 + local_echo -n "Connecting to $1@$2..." + do_until_true ssh_prewrapper -q $1@$2 true 1>&2 } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index f2c7b5e3cca..4268ec6d4fa 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -38,8 +38,9 @@ function instance_execute() { "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") instance_id=$(create_instance "$user_data") + public_dns_name=$(query_public_dns_name $instance_id) - wait_for_ssh_connection $instance_id + wait_for_ssh_connection $ssh_user $public_dns_name header "Event and user creation" diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index baddce684d4..c01d625f898 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -40,8 +40,9 @@ function master_instance_execute() { "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") instance_id=$(create_instance "$user_data_master") + public_dns_name=$(query_public_dns_name $instance_id) - wait_for_ssh_connection $instance_id + wait_for_ssh_connection $ssh_user $public_dns_name header "Event and user creation" From 4a6d9ade304af8d337cfe845277396db347ecfe0 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Thu, 1 Feb 2018 12:57:31 +0100 Subject: [PATCH 080/214] Moved keypair and keyname to region config files. SSH user now adjustable within shared instance scenario. --- .../aws-automation/lib/config_eu_west_1.sh | 6 ++++-- .../aws-automation/lib/config_eu_west_2.sh | 7 ++++++- .../aws-automation/lib/functions_io.sh | 8 +++---- .../aws-automation/lib/scenario_instance.sh | 2 ++ .../lib/scenario_master_instance.sh | 2 ++ .../lib/scenario_shared_instance.sh | 21 ++++++++++--------- configuration/aws-automation/lib/variables.sh | 3 --- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 7352b62dda8..8a3c524d4d1 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -1,6 +1,6 @@ # Variables for region "eu-west-1" (Ireland) -default_key_name=Leon +default_key_name='Leon' default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' # HTTPS Listener of Application Load Balancer Sailing-eu-west-1 @@ -11,6 +11,7 @@ instance_security_group_ids=sg-eaf31e85 # SAP Sailing Analytics App HVM 1.61 image_id='ami-7719840e' +image_ssh_user="root" certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' elb_security_group_ids= @@ -19,4 +20,5 @@ mongodb_port= alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server -default_super_instance='' +default_super_instance_dns_name='' +default_ssh_user=$image_ssh_user diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 05ece9b0988..c17d1cd71eb 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -1,5 +1,8 @@ # Variables for region "eu-west-2" (London) +default_key_name='leonradeck-keypair' +default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' + # HTTPS Listener of Application Load Balancer DummyALB listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' @@ -8,6 +11,7 @@ instance_security_group_ids=sg-871732ee # SAP Sailing Analytics App HVM 1.56 image_id=ami-39f3e25d +image_ssh_user="root" certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' elb_security_group_ids=sg-871732ee @@ -15,4 +19,5 @@ mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' -default_super_instance='ec2-35-176-58-204.eu-west-2.compute.amazonaws.com' +default_super_instance_dns_name='' +default_ssh_user=$image_ssh_user diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 4e1ef9d9f4c..4e198512cc5 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -101,11 +101,11 @@ function require_public_dns_name(){ } function require_ssh_user(){ - require_variable "$ssh_user_param" ssh_user "" "$ssh_user_ask_message" + require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" } -function require_super_instance(){ - require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" +function require_super_instance_dns_name(){ + require_variable "$super_instance_dns_name_param" super_instance_dns_name "$default_super_instance_dns_name" "$super_instance_dns_name_message" } function require_description(){ @@ -137,7 +137,7 @@ user_username_ask_message="Please enter the username of your new user: " user_password_ask_message="Please enter the password of your new user: " public_dns_name_ask_message="Please enter the public dns name: " ssh_user_ask_message="Please enter the ssh user: " -super_instance_message="Please enter the dns name of the superior instance: " +super_instance_dns_name_message="Please enter the dns name of the superior instance: " description_message="Please enter a description for the server: " contact_person_message="Please enter the name of a contact person: " contact_email_message="Please enter the email of the contact person: " diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 4268ec6d4fa..bb097b4879b 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -33,6 +33,8 @@ function instance_require(){ function instance_execute() { header "Instance Initialization" + ssh_user=$image_ssh_user + local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index c01d625f898..d96739794ef 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -35,6 +35,8 @@ function master_instance_require(){ function master_instance_execute() { header "Master Instance Initialization" + ssh_user=$image_ssh_user + local user_data_master=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-master-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 74f3aeec2f5..dc8ca6b4d65 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -15,7 +15,8 @@ function shared_instance_start(){ # the user will be prompted to enter a value # ----------------------------------------------------------- function shared_instance_require(){ - require_super_instance + require_super_instance_dns_name + require_ssh_user require_key_name require_key_file @@ -41,7 +42,7 @@ function shared_instance_check_preconditions(){ echo "Checking if directory $server_dir exists already..." execute_remote "[ ! -d $server_dir ]" - echo "Checking if ssh connection $ssh_user@$super_instance is working..." + echo "Checking if ssh connection $ssh_user@$super_instance_dns_name is working..." execute_remote ls } @@ -103,29 +104,29 @@ function shared_instance_execute() { header "Event and user creation" # get access token - access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) + access_token=$(get_access_token $admin_username $admin_password $super_instance_dns_name $server_port) # create event - event_id=$(create_event $access_token $super_instance $server_port $instance_name) + event_id=$(create_event $access_token $super_instance_dns_name $server_port $instance_name) # change admin password - change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password + change_admin_password $access_token $super_instance_dns_name $server_port $admin_username $new_admin_password # create new user - user=$(create_new_user $access_token $super_instance $server_port $user_username $user_password) + user=$(create_new_user $access_token $super_instance_dns_name $server_port $user_username $user_password) header "Configuring ALB" local target_group_arn=$(create_target_group "S-shared-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "$server_port" "5" "4" "2" "2" - register_targets $target_group_arn $(get_instance_id $super_instance) + register_targets $target_group_arn $(get_instance_id $super_instance_dns_name) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) header "Configuring Apache" - append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance_dns_name $server_port local_echo "Reloading httpd..." out=$(execute_remote "/etc/init.d/httpd reload") @@ -153,9 +154,9 @@ function find_first_unused_port(){ } function execute_remote(){ - ssh_wrapper $ssh_user@$super_instance "$@" + ssh_wrapper $ssh_user@$super_instance_dns_name "$@" } function get_last_used_port(){ - ssh_prewrapper $ssh_user@$super_instance "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" + ssh_prewrapper $ssh_user@$super_instance_dns_name "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" } diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 7da50cf2078..a7f4ecf018b 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -2,8 +2,6 @@ # START EDIT default_region=eu-west-2 -default_key_name=leonradeck-keypair -default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' default_instance_type=t2.medium default_server_startup_notify=leon.radeck@sap.com default_new_admin_password=admin @@ -35,7 +33,6 @@ tag_specifications="\'ResourceType=instance,Tags=[{Key=Name,Value=%s}]\'" admin_username=admin admin_password=admin instance_count=1 -ssh_user=root default_instance_name="$script_start_time" default_instance_short_name="$timestamp" sailing_0='/home/sailing/servers/server/logs/sailing0.log.0' From bee26e5499bddf5b9d154520811c3d3d1e5cd73f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Thu, 1 Feb 2018 13:10:59 +0100 Subject: [PATCH 081/214] Ask for ssh user always --- configuration/aws-automation/lib/config_eu_west_1.sh | 5 ++--- configuration/aws-automation/lib/config_eu_west_2.sh | 3 +-- configuration/aws-automation/lib/scenario_instance.sh | 3 +-- configuration/aws-automation/lib/scenario_master_instance.sh | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 8a3c524d4d1..1660f09b5c1 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -11,7 +11,6 @@ instance_security_group_ids=sg-eaf31e85 # SAP Sailing Analytics App HVM 1.61 image_id='ami-7719840e' -image_ssh_user="root" certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' elb_security_group_ids= @@ -20,5 +19,5 @@ mongodb_port= alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server -default_super_instance_dns_name='' -default_ssh_user=$image_ssh_user +default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' +default_ssh_user='root' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index c17d1cd71eb..a105b6a3f4c 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -11,7 +11,6 @@ instance_security_group_ids=sg-871732ee # SAP Sailing Analytics App HVM 1.56 image_id=ami-39f3e25d -image_ssh_user="root" certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' elb_security_group_ids=sg-871732ee @@ -20,4 +19,4 @@ mongodb_port=27017 alb_domain='dummy2.sapsailing.com' default_super_instance_dns_name='' -default_ssh_user=$image_ssh_user +default_ssh_user="root" diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index bb097b4879b..2082b639eab 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -18,6 +18,7 @@ function instance_require(){ require_instance_type require_instance_name require_instance_short_name + require_ssh_user require_build_version require_key_name require_key_file @@ -33,8 +34,6 @@ function instance_require(){ function instance_execute() { header "Instance Initialization" - ssh_user=$image_ssh_user - local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index d96739794ef..186bfdace56 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -20,6 +20,7 @@ function master_instance_require(){ require_instance_type require_instance_name require_instance_short_name + require_ssh_user require_build_version require_key_name require_key_file From a527f7798926eb0bcbad7ad05d031e4e1c4f2751 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Thu, 1 Feb 2018 13:25:02 +0100 Subject: [PATCH 082/214] Key name not needed for shared instance scenario. --- configuration/aws-automation/lib/scenario_shared_instance.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index dc8ca6b4d65..16aa01c69c3 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -18,7 +18,6 @@ function shared_instance_require(){ require_super_instance_dns_name require_ssh_user - require_key_name require_key_file require_instance_name require_instance_short_name From 03fddc92bdcf24464405ca284c1c44fb89a5da5f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Thu, 1 Feb 2018 13:27:20 +0100 Subject: [PATCH 083/214] Changed host and port of mongodb conf of eu-west-1 --- configuration/aws-automation/lib/config_eu_west_1.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 1660f09b5c1..00de5919e29 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -14,8 +14,8 @@ image_id='ami-7719840e' certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' elb_security_group_ids= -mongodb_host= -mongodb_port= +mongodb_host='dbserver.internal.sapsailing.com' +mongodb_port='10202' alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server From 1bf29836d77970478baec91ef07b6bbad5c2775f Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 2 Feb 2018 15:12:41 +0100 Subject: [PATCH 084/214] Bug 4380: Moved race slicing UI to its own class and changed how actions are attached to charts to ensure a consistent layout --- .../charts/AbstractCompetitorRaceChart.java | 5 - .../shared/charts/AbstractRaceChart.java | 18 +- .../shared/charts/ChartCssResources.java | 4 +- .../shared/charts/ChartZoomChangedEvent.java | 2 +- .../shared/charts/ChartZoomResetEvent.java | 2 +- .../gwt/ui/client/shared/charts/Charts.css | 71 ++++---- .../charts/MultiCompetitorRaceChart.java | 126 -------------- .../gwt/ui/raceboard/RaceBoardPanel.java | 4 + .../gwt/ui/raceboard/SliceRaceHandler.java | 163 ++++++++++++++++++ 9 files changed, 211 insertions(+), 184 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index 2f4ec0e29eb..abe22813291 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -37,7 +37,6 @@ import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.Widget; @@ -132,10 +131,6 @@ public abstract class AbstractCompetitorRaceChart extends A busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f); busyIndicator.setPanelStyleClass(chartsCss.busyIndicatorStyle()); busyIndicator.setImageStyleClass(chartsCss.busyIndicatorImageStyle()); - settingsButton = createSettingsButton(); - settingsButton.setStyleName(chartsCss.settingsButtonStyle()); - settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); - add(settingsButton); toolbar.addStyleName(chartsCss.toolbar()); - add(toolbar); + add(toolbar); + settingsButton = createSettingsButton(); + settingsButton.setStyleName(chartsCss.settingsButtonBackgroundImage()); + addToolbarButton(settingsButton); initWidget(rootPanel); getElement().getStyle().setMarginRight(12, Unit.PX); getElement().getStyle().setMarginLeft(12, Unit.PX); @@ -130,13 +129,8 @@ public abstract class AbstractRaceChart extends A */ protected abstract Button createSettingsButton(); - protected void createExtraToolbarElements(FlowPanel panel) { - } - - @Override - protected final void onLoad() { - createExtraToolbarElements(toolbar); - super.onLoad(); + public void addToolbarButton(Button button) { + toolbar.insert(button, 0); } /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java index a33c6e03454..682bd93df19 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java @@ -19,10 +19,10 @@ public interface ChartCssResources extends ClientBundle { String busyIndicatorImageStyle(); - String settingsButtonStyle(); - String settingsButtonBackgroundImage(); + String sliceButtonBackgroundImage(); + String chartStyle(); String toolbar(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java index 8dd83d61861..f784fbeca73 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java @@ -9,7 +9,7 @@ public class ChartZoomChangedEvent extends GwtEvent TYPE = new Type<>(); private final Date rangeStart; private final Date rangeEnd; - interface Handler extends EventHandler { + public interface Handler extends EventHandler { void handleZoomChanged(ChartZoomChangedEvent cze); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java index 2779880508b..6fb5330f082 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java @@ -6,7 +6,7 @@ import com.google.gwt.event.shared.GwtEvent; public class ChartZoomResetEvent extends GwtEvent { public static final Type TYPE = new Type<>(); - interface Handler extends EventHandler { + public interface Handler extends EventHandler { void handleZoomReset(ChartZoomResetEvent czre); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css index 27b28920ae9..1ef6df6aa96 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css @@ -1,51 +1,48 @@ .busyIndicatorStyle { - display: table-cell; - vertical-align: middle; - text-align: center; + display: table-cell; + vertical-align: middle; + text-align: center; } .busyIndicatorImageStyle { - -} - -.settingsButtonStyle { - position: absolute; - right: 0px; - bottom: 10px; - z-index: 1; - margin-top: 14px; - margin-right: 4px; - border: none; -} - -.settingsButtonStyle:focus { - outline: 0 !important; -} - -.settingsButtonStyle:hover { - border: 1px solid #ececec; + } .chartStyle { - position: absolute; - left: 0px; - top: 0px; + position: absolute; + left: 0px; + top: 0px; } .toolbar { - position: absolute; - right: 2.6em; - height: 2em; - width: 200px; - bottom: 4px; - - text-align: right; + position: absolute; + right: 4px; + bottom: 10px; + + text-align: right; z-index: 1; } +.toolbar>button { + margin-top: 8px; + width: 2em; + height: 2em; + background-size: contain; + border: none; + display: block; +} + +.toolbar>button:focus { + outline: 0 !important; +} + +.toolbar>button:hover { + border: 1px solid #ececec; +} @sprite .settingsButtonBackgroundImage { - gwt-image: "settingsButton"; - width: 2em; - height: 2em; - background-size: contain; -} \ No newline at end of file + gwt-image: "settingsButton"; +} + +@sprite .sliceButtonBackgroundImage { + gwt-image: "settingsButton"; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 95162131dc8..3452cdd856a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,27 +1,15 @@ package com.sap.sailing.gwt.ui.client.shared.charts; import java.util.UUID; -import java.util.function.Consumer; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.TextBox; -import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; -import com.sap.sailing.gwt.ui.shared.SliceRacePreperationDTO; -import com.sap.sse.common.TimeRange; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; @@ -44,10 +32,6 @@ import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart implements Component { private final MultiCompetitorRaceChartLifecycle lifecycle; - private final Button splitButtonUi = new Button(StringMessages.INSTANCE.sliceRace()); - - private TimeRange visibleRange; - private final UUID eventId; /** * Creates a Chart used for example in the Raceboard to display various additional data. @@ -66,78 +50,13 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChartcheckIfMaySliceSelectedRegattaAndRace(e)); - addChartZoomResetHandler((e)->splitButtonUi.setVisible(false)); - splitButtonUi.addClickHandler((e)->doSlice()); } - private void doSlice() { - if (visibleRange != null) { - sailingService.prepareForSlicingOfRace(selectedRaceIdentifier, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(StringMessages.INSTANCE.errorWhilePreparingToSliceARace()); - } - - @Override - public void onSuccess(SliceRacePreperationDTO sliceRacePreperatioData) { - new SlicedRaceNameDialog(sliceRacePreperatioData, slicedRaceName -> { - sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), visibleRange.to(), new AsyncCallback() { - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(StringMessages.INSTANCE.errorWhileSlicingARace()); - } - - @Override - public void onSuccess(RegattaAndRaceIdentifier result) { - new TrackedRaceCreationResultDialog(StringMessages.INSTANCE.sliceRace(), - StringMessages.INSTANCE.slicingARaceWasSuccessful(), - eventId, result.getRegattaName(), result.getRaceName(), - leaderboardName, leaderboardGroupName).show(); - } - }); - }).show(); - } - }); - } - } - - private void checkIfMaySliceSelectedRegattaAndRace(final ChartZoomChangedEvent e) { - // TODO: we could cache result - visibleRange = null; - sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { - @Override - public void onSuccess(Boolean result) { - boolean yesWeCan = Boolean.TRUE.equals(result); - splitButtonUi.setVisible(yesWeCan); - if (yesWeCan) { - visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), new MillisecondsTimePoint(e.getRangeEnd())); - } - } - - @Override - public void onFailure(Throwable caught) { - splitButtonUi.setVisible(false); - } - }); - } - - @Override protected Button createSettingsButton() { Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); return settingsButton; } - - @Override - protected void createExtraToolbarElements(FlowPanel panel) { - panel.add(splitButtonUi); - } - @Override public MultiCompetitorRaceChartSettings getSettings() { @@ -197,49 +116,4 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart { - - private final TextBox raceNameInput; - - public SlicedRaceNameDialog(final SliceRacePreperationDTO sliceRacePreperatioData, final Consumer okCallback) { - super(StringMessages.INSTANCE.sliceRace(), StringMessages.INSTANCE.enterNameForSlicedRace(), StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { - @Override - public String getErrorMessage(String valueToValidate) { - if (valueToValidate == null || valueToValidate.isEmpty()) { - return StringMessages.INSTANCE.raceNameIsRequired(); - } - if (sliceRacePreperatioData.getAlreadyUsedNames().contains(valueToValidate)) { - return StringMessages.INSTANCE.raceNameIsAlreadyUsed(); - } - return null; - } - }, new DialogCallback() { - @Override - public void ok(String editedObject) { - okCallback.accept(editedObject); - } - - @Override - public void cancel() { - } - }); - raceNameInput = createTextBox(sliceRacePreperatioData.getProposedRaceName()); - raceNameInput.addAttachHandler(e -> { - if (e.isAttached()) { - Scheduler.get().scheduleDeferred(() -> raceNameInput.setFocus(true)); - } - }); - } - - @Override - protected Widget getAdditionalWidget() { - return raceNameInput; - } - - @Override - protected String getResult() { - return raceNameInput.getValue().trim(); - } - } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 0ac42917835..931a7b65544 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -365,6 +365,10 @@ public class RaceBoardPanel competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); competitorChart.setVisible(false); competitorChart.updateSettings(multiCompetitorRaceChartSettings); + + new SliceRaceHandler(sailingService, errorReporter, competitorChart, selectedRaceIdentifier, + leaderboardGroupName, leaderboardName, event); + componentsForSideBySideViewer.add(competitorChart); windChart = new WindChart(this, getComponentContext(), windChartLifecycle, sailingService, selectedRaceIdentifier, timer, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java new file mode 100644 index 00000000000..853b4411f87 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -0,0 +1,163 @@ +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.UUID; +import java.util.function.Consumer; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.charts.ChartCssResources; +import com.sap.sailing.gwt.ui.client.shared.charts.ChartZoomChangedEvent; +import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorRaceChart; +import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; +import com.sap.sailing.gwt.ui.shared.SliceRacePreperationDTO; +import com.sap.sse.common.TimeRange; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.common.impl.TimeRangeImpl; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class SliceRaceHandler { + private final Button splitButtonUi; + + private TimeRange visibleRange; + + private final String leaderboardGroupName; + + private final String leaderboardName; + + private final UUID eventId; + + private final SailingServiceAsync sailingService; + + private final ErrorReporter errorReporter; + + private final RegattaAndRaceIdentifier selectedRaceIdentifier; + + public SliceRaceHandler(SailingServiceAsync sailingService, final ErrorReporter errorReporter, + MultiCompetitorRaceChart competitorRaceChart, RegattaAndRaceIdentifier selectedRaceIdentifier, + final String leaderboardGroupName, String leaderboardName, UUID eventId) { + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.selectedRaceIdentifier = selectedRaceIdentifier; + this.leaderboardGroupName = leaderboardGroupName; + this.leaderboardName = leaderboardName; + this.eventId = eventId; + splitButtonUi = new Button(); + splitButtonUi.setStyleName(ChartCssResources.INSTANCE.css().sliceButtonBackgroundImage()); + splitButtonUi.setTitle(StringMessages.INSTANCE.sliceRace()); + competitorRaceChart.addToolbarButton(splitButtonUi); + splitButtonUi.setVisible(false); + competitorRaceChart.addChartZoomChangedHandler(this::checkIfMaySliceSelectedRegattaAndRace); + competitorRaceChart.addChartZoomResetHandler(e -> splitButtonUi.setVisible(false)); + splitButtonUi.addClickHandler((e) -> doSlice()); + } + + private void doSlice() { + if (visibleRange != null) { + sailingService.prepareForSlicingOfRace(selectedRaceIdentifier, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(StringMessages.INSTANCE.errorWhilePreparingToSliceARace()); + } + + @Override + public void onSuccess(SliceRacePreperationDTO sliceRacePreperatioData) { + new SlicedRaceNameDialog(sliceRacePreperatioData, slicedRaceName -> { + sailingService.sliceRace(selectedRaceIdentifier, slicedRaceName, visibleRange.from(), + visibleRange.to(), new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + errorReporter + .reportError(StringMessages.INSTANCE.errorWhileSlicingARace()); + } + + @Override + public void onSuccess(RegattaAndRaceIdentifier result) { + new TrackedRaceCreationResultDialog(StringMessages.INSTANCE.sliceRace(), + StringMessages.INSTANCE.slicingARaceWasSuccessful(), eventId, + result.getRegattaName(), result.getRaceName(), leaderboardName, + leaderboardGroupName).show(); + } + }); + }).show(); + } + }); + } + } + + private void checkIfMaySliceSelectedRegattaAndRace(final ChartZoomChangedEvent e) { + // TODO: we could cache result + visibleRange = null; + sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { + @Override + public void onSuccess(Boolean result) { + boolean yesWeCan = Boolean.TRUE.equals(result); + splitButtonUi.setVisible(yesWeCan); + if (yesWeCan) { + visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), + new MillisecondsTimePoint(e.getRangeEnd())); + } + } + + @Override + public void onFailure(Throwable caught) { + splitButtonUi.setVisible(false); + } + }); + } + + private static class SlicedRaceNameDialog extends DataEntryDialog { + + private final TextBox raceNameInput; + + public SlicedRaceNameDialog(final SliceRacePreperationDTO sliceRacePreperatioData, + final Consumer okCallback) { + super(StringMessages.INSTANCE.sliceRace(), StringMessages.INSTANCE.enterNameForSlicedRace(), + StringMessages.INSTANCE.ok(), StringMessages.INSTANCE.cancel(), new Validator() { + @Override + public String getErrorMessage(String valueToValidate) { + if (valueToValidate == null || valueToValidate.isEmpty()) { + return StringMessages.INSTANCE.raceNameIsRequired(); + } + if (sliceRacePreperatioData.getAlreadyUsedNames().contains(valueToValidate)) { + return StringMessages.INSTANCE.raceNameIsAlreadyUsed(); + } + return null; + } + }, new DialogCallback() { + @Override + public void ok(String editedObject) { + okCallback.accept(editedObject); + } + + @Override + public void cancel() { + } + }); + raceNameInput = createTextBox(sliceRacePreperatioData.getProposedRaceName()); + raceNameInput.addAttachHandler(e -> { + if (e.isAttached()) { + Scheduler.get().scheduleDeferred(() -> raceNameInput.setFocus(true)); + } + }); + } + + @Override + protected Widget getAdditionalWidget() { + return raceNameInput; + } + + @Override + protected String getResult() { + return raceNameInput.getValue().trim(); + } + } +} From 3e89dadc302b47e5eb289d259d46377a97ace702 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 11:10:00 +0100 Subject: [PATCH 085/214] Bug 4380: Moved styling and fixed button background --- .../shared/charts/ChartCssResources.java | 2 -- .../gwt/ui/client/shared/charts/Charts.css | 5 +--- .../gwt/ui/raceboard/SliceRaceHandler.gss | 3 +++ .../gwt/ui/raceboard/SliceRaceHandler.java | 23 ++++++++++++++++++- 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java index 682bd93df19..6b1c9c4b8d8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartCssResources.java @@ -21,8 +21,6 @@ public interface ChartCssResources extends ClientBundle { String settingsButtonBackgroundImage(); - String sliceButtonBackgroundImage(); - String chartStyle(); String toolbar(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css index 1ef6df6aa96..44a086389e8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/Charts.css @@ -26,6 +26,7 @@ margin-top: 8px; width: 2em; height: 2em; + background-color: transparent; background-size: contain; border: none; display: block; @@ -42,7 +43,3 @@ @sprite .settingsButtonBackgroundImage { gwt-image: "settingsButton"; } - -@sprite .sliceButtonBackgroundImage { - gwt-image: "settingsButton"; -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss new file mode 100644 index 00000000000..974fd0b3e45 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss @@ -0,0 +1,3 @@ +.sliceButtonBackgroundImage { + background-image: resourceUrl('settingsButton'); +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index 853b4411f87..bd6851e0225 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -3,7 +3,11 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.UUID; import java.util.function.Consumer; +import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.TextBox; @@ -23,6 +27,20 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class SliceRaceHandler { + interface Resources extends ClientBundle { + @Source("com/sap/sailing/gwt/ui/client/images/SAP_RV_Settings.png") + ImageResource settingsButton(); + + @Source("SliceRaceHandler.gss") + Styles style(); + } + + interface Styles extends CssResource { + String sliceButtonBackgroundImage(); + } + + private final Styles styles = GWT.create(Resources.class).style(); + private final Button splitButtonUi; private TimeRange visibleRange; @@ -48,8 +66,11 @@ public class SliceRaceHandler { this.leaderboardGroupName = leaderboardGroupName; this.leaderboardName = leaderboardName; this.eventId = eventId; + + styles.ensureInjected(); + splitButtonUi = new Button(); - splitButtonUi.setStyleName(ChartCssResources.INSTANCE.css().sliceButtonBackgroundImage()); + splitButtonUi.setStyleName(styles.sliceButtonBackgroundImage()); splitButtonUi.setTitle(StringMessages.INSTANCE.sliceRace()); competitorRaceChart.addToolbarButton(splitButtonUi); splitButtonUi.setVisible(false); From 07f7f433042c0d7d7cc8b8043f6237d18057f14c Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 13:55:18 +0100 Subject: [PATCH 086/214] Bug 4380: Implemented visibility of the slice race action based on MANAGE_TRACKED_RACES permission --- .../gwt/ui/raceboard/RaceBoardPanel.java | 2 +- .../gwt/ui/raceboard/SliceRaceHandler.java | 75 +++++++++++++------ 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 931a7b65544..d7f72727365 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -366,7 +366,7 @@ public class RaceBoardPanel competitorChart.setVisible(false); competitorChart.updateSettings(multiCompetitorRaceChartSettings); - new SliceRaceHandler(sailingService, errorReporter, competitorChart, selectedRaceIdentifier, + new SliceRaceHandler(sailingService, userService, errorReporter, competitorChart, selectedRaceIdentifier, leaderboardGroupName, leaderboardName, event); componentsForSideBySideViewer.add(competitorChart); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index bd6851e0225..8dd705ff96e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -13,9 +13,10 @@ import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.security.Permission; +import com.sap.sailing.domain.common.security.SailingPermissionsForRoleProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.ChartCssResources; import com.sap.sailing.gwt.ui.client.shared.charts.ChartZoomChangedEvent; import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorRaceChart; import com.sap.sailing.gwt.ui.client.shared.race.TrackedRaceCreationResultDialog; @@ -25,6 +26,9 @@ import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.dialog.DataEntryDialog; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.shared.UserDTO; public class SliceRaceHandler { interface Resources extends ClientBundle { @@ -53,14 +57,19 @@ public class SliceRaceHandler { private final SailingServiceAsync sailingService; + private final UserService userService; + private final ErrorReporter errorReporter; private final RegattaAndRaceIdentifier selectedRaceIdentifier; + + private boolean canSlice = false; - public SliceRaceHandler(SailingServiceAsync sailingService, final ErrorReporter errorReporter, + public SliceRaceHandler(SailingServiceAsync sailingService, UserService userService, final ErrorReporter errorReporter, MultiCompetitorRaceChart competitorRaceChart, RegattaAndRaceIdentifier selectedRaceIdentifier, final String leaderboardGroupName, String leaderboardName, UUID eventId) { this.sailingService = sailingService; + this.userService = userService; this.errorReporter = errorReporter; this.selectedRaceIdentifier = selectedRaceIdentifier; this.leaderboardGroupName = leaderboardGroupName; @@ -75,8 +84,47 @@ public class SliceRaceHandler { competitorRaceChart.addToolbarButton(splitButtonUi); splitButtonUi.setVisible(false); competitorRaceChart.addChartZoomChangedHandler(this::checkIfMaySliceSelectedRegattaAndRace); - competitorRaceChart.addChartZoomResetHandler(e -> splitButtonUi.setVisible(false)); + competitorRaceChart.addChartZoomResetHandler(e -> { + visibleRange = null; + updateVisibility(); + }); splitButtonUi.addClickHandler((e) -> doSlice()); + + sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { + @Override + public void onSuccess(Boolean result) { + canSlice = Boolean.TRUE.equals(result); + updateVisibility(); + } + + @Override + public void onFailure(Throwable caught) { + canSlice = false; + updateVisibility(); + } + }); + + final UserStatusEventHandler userStatusEventHandler = (user, preAuthenticated) -> { + updateVisibility(); + }; + splitButtonUi.addAttachHandler(e -> { + if (e.isAttached()) { + userService.addUserStatusEventHandler(userStatusEventHandler); + } else { + userService.removeUserStatusEventHandler(userStatusEventHandler); + } + }); + } + + private void updateVisibility() { + splitButtonUi.setVisible(canSlice && visibleRange != null && allowsEditing()); + } + + private boolean allowsEditing() { + final UserDTO currentUser = userService.getCurrentUser(); + return currentUser != null + && currentUser.hasPermission(Permission.MANAGE_TRACKED_RACES.getStringPermission(), + SailingPermissionsForRoleProvider.INSTANCE); } private void doSlice() { @@ -115,24 +163,9 @@ public class SliceRaceHandler { } private void checkIfMaySliceSelectedRegattaAndRace(final ChartZoomChangedEvent e) { - // TODO: we could cache result - visibleRange = null; - sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { - @Override - public void onSuccess(Boolean result) { - boolean yesWeCan = Boolean.TRUE.equals(result); - splitButtonUi.setVisible(yesWeCan); - if (yesWeCan) { - visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), - new MillisecondsTimePoint(e.getRangeEnd())); - } - } - - @Override - public void onFailure(Throwable caught) { - splitButtonUi.setVisible(false); - } - }); + visibleRange = new TimeRangeImpl(new MillisecondsTimePoint(e.getRangeStart()), + new MillisecondsTimePoint(e.getRangeEnd())); + updateVisibility(); } private static class SlicedRaceNameDialog extends DataEntryDialog { From 02fef4979b67e32793588fda8e960fa0e2a06464 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 13:55:55 +0100 Subject: [PATCH 087/214] Bug 4380: Using another icon for the slice action --- .../com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss | 2 +- .../com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss index 974fd0b3e45..8c7613d52e4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.gss @@ -1,3 +1,3 @@ .sliceButtonBackgroundImage { - background-image: resourceUrl('settingsButton'); + background-image: resourceUrl('sliceButton'); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index 8dd705ff96e..93064366510 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -32,8 +32,8 @@ import com.sap.sse.security.ui.shared.UserDTO; public class SliceRaceHandler { interface Resources extends ClientBundle { - @Source("com/sap/sailing/gwt/ui/client/images/SAP_RV_Settings.png") - ImageResource settingsButton(); + @Source("com/sap/sailing/gwt/ui/client/images/close_time_range.png") + ImageResource sliceButton(); @Source("SliceRaceHandler.gss") Styles style(); From 83465131dc2a9d6e245ac92288d49ae1ffd47ee5 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 13:56:32 +0100 Subject: [PATCH 088/214] Bug 4380: consistent naming --- .../gwt/ui/raceboard/SliceRaceHandler.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index 93064366510..6a457a8ed1a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -45,7 +45,7 @@ public class SliceRaceHandler { private final Styles styles = GWT.create(Resources.class).style(); - private final Button splitButtonUi; + private final Button sliceButtonUi; private TimeRange visibleRange; @@ -78,17 +78,17 @@ public class SliceRaceHandler { styles.ensureInjected(); - splitButtonUi = new Button(); - splitButtonUi.setStyleName(styles.sliceButtonBackgroundImage()); - splitButtonUi.setTitle(StringMessages.INSTANCE.sliceRace()); - competitorRaceChart.addToolbarButton(splitButtonUi); - splitButtonUi.setVisible(false); + sliceButtonUi = new Button(); + sliceButtonUi.setStyleName(styles.sliceButtonBackgroundImage()); + sliceButtonUi.setTitle(StringMessages.INSTANCE.sliceRace()); + competitorRaceChart.addToolbarButton(sliceButtonUi); + sliceButtonUi.setVisible(false); competitorRaceChart.addChartZoomChangedHandler(this::checkIfMaySliceSelectedRegattaAndRace); competitorRaceChart.addChartZoomResetHandler(e -> { visibleRange = null; updateVisibility(); }); - splitButtonUi.addClickHandler((e) -> doSlice()); + sliceButtonUi.addClickHandler((e) -> doSlice()); sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { @Override @@ -107,7 +107,7 @@ public class SliceRaceHandler { final UserStatusEventHandler userStatusEventHandler = (user, preAuthenticated) -> { updateVisibility(); }; - splitButtonUi.addAttachHandler(e -> { + sliceButtonUi.addAttachHandler(e -> { if (e.isAttached()) { userService.addUserStatusEventHandler(userStatusEventHandler); } else { @@ -117,7 +117,7 @@ public class SliceRaceHandler { } private void updateVisibility() { - splitButtonUi.setVisible(canSlice && visibleRange != null && allowsEditing()); + sliceButtonUi.setVisible(canSlice && visibleRange != null && allowsEditing()); } private boolean allowsEditing() { From 9e6dabae474ba495a73a68835497c8edacd7a4ea Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 14:14:56 +0100 Subject: [PATCH 089/214] Bug 4380: solved TODO --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 3 ++- .../gateway/trackfiles/impl/ExpeditionAllInOneImporter.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) mode change 100755 => 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java old mode 100755 new mode 100644 index 00a8640c4bf..70ff6c658f4 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7165,7 +7165,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - // TODO do we need to wait or is the TrackedRace guaranteed to be reachable after calling startTracking? + // This call waits until the RaceDefinition exists so that we can be sure that the TrackedRace also exists + // after the call returns raceHandle.getRace(); final DynamicTrackedRace trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 7c1a92c37ef..1b73e6b1b0d 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -253,7 +253,8 @@ public class ExpeditionAllInOneImporter { final RaceHandle raceHandle = adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - // TODO do we need to wait or is the TrackedRace guaranteed to be reachable after calling startTracking? + // This call waits until the RaceDefinition exists so that we can be sure that the TrackedRace also exists + // after the call returns raceHandle.getRace(); final DynamicTrackedRace trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); From eb76dff03484a29eca5d59b2d99af4e486e6bef7 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 14:15:52 +0100 Subject: [PATCH 090/214] Bug 4380: removed TODO that was resolved some time ago --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 70ff6c658f4..324750d8e6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7032,7 +7032,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S if (!canSliceRace(raceIdentifier)) { throw new RuntimeException("Can not slice race"); } - // TODO check if the race column name is already used final String trackedRaceName = newRaceColumnName; final RegattaIdentifier regattaIdentifier = new RegattaName(raceIdentifier.getRegattaName()); final Regatta regatta = getService().getRegatta(regattaIdentifier); From 31940692cc6bd413a9d46b06b4ec9ffa32160d7b Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 14:17:05 +0100 Subject: [PATCH 091/214] Bug 4380: Only smartphone-tracked races can be sliced. Checking for the denotation isn't sufficient to ensure this. --- .../domain/common/racelog/tracking/RaceLogTrackingState.java | 4 ++++ .../com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java index 9f18093d622..3da451a8474 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java @@ -27,4 +27,8 @@ public enum RaceLogTrackingState { public boolean isForTracking() { return this != NOT_A_RACELOG_TRACKED_RACE; } + + public boolean isTracking() { + return this == TRACKING; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 324750d8e6b..e83f5ff8d21 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -6991,10 +6991,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } private boolean isSmartphoneTrackingEnabled(DynamicTrackedRace trackedRace) { - // TODO not sure if this check is sufficient for (RaceLog raceLog : trackedRace.getAttachedRaceLogs()) { RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze(); - if (raceLogTrackingState.isForTracking()) { + if (raceLogTrackingState.isTracking()) { return true; } } From 56a90ef0b1fd27194976b81af4569efb199aef71 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 14:17:23 +0100 Subject: [PATCH 092/214] Bug 4380: Added JavaDocs --- .../sap/sailing/gwt/ui/client/SailingService.java | 13 +++++++++++++ .../sailing/gwt/ui/raceboard/SliceRaceHandler.java | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 554ee526047..23a89a8e530 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -11,6 +11,7 @@ import java.util.UUID; import com.google.gwt.user.client.rpc.RemoteService; import com.sap.sailing.domain.abstractlog.Revokable; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; +import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.DataImportProgress; import com.sap.sailing.domain.common.DetailType; @@ -48,6 +49,7 @@ import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogExcep import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; import com.sap.sailing.domain.common.racelog.tracking.TransformationException; import com.sap.sailing.domain.common.tracking.impl.PreciseCompactGPSFixMovingImpl.PreciseCompactPosition; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.tracking.TrackedRace; @@ -711,9 +713,20 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS List getRaceDisplayNamesFromLeaderboard(String leaderboardName, List raceColumnNames) throws NotFoundException; + /** + * Returns {@code true} if the given race can be sliced. Only Smarthphone tracked races can be sliced. + * In addition the race must be part of a {@link RegattaLeaderboard}. + */ boolean canSliceRace(RegattaAndRaceIdentifier raceIdentifier); + /** + * Slices a new race from the race specified by the given {@link RegattaAndRaceIdentifier} using the given time range. + * A new {@link RaceColumn} with the given name is added to the {@link RegattaLeaderboard}. + */ RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo); + /** + * Returns specific data needed for the slicing UI. + */ SliceRacePreperationDTO prepareForSlicingOfRace(RegattaAndRaceIdentifier raceIdentifier); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index 6a457a8ed1a..3ca06d467ec 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -30,6 +30,10 @@ import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.client.UserStatusEventHandler; import com.sap.sse.security.ui.shared.UserDTO; +/** + * Encapsulates the UI to slice a race from an existing race. + * The slice functionality is added as an extra action to a given {@link MultiCompetitorRaceChart}. + */ public class SliceRaceHandler { interface Resources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/close_time_range.png") From e6484e2c2a049c541da8fb47d36e51686bde7ffb Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 15:01:40 +0100 Subject: [PATCH 093/214] Bug 4380: Reverted changes that aren't needed anymore with the current implementation --- .../ui/client/shared/charts/AbstractCompetitorRaceChart.java | 4 ++-- .../gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java | 4 +--- .../java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index abe22813291..4cfb01c7faf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -93,8 +93,8 @@ public abstract class AbstractCompetitorRaceChart, Series> dataSeriesForDetailTypeAndCompetitor = new HashMap<>(); private final Map, Series> markPassingSeriesByCompetitor = new HashMap<>(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index 3452cdd856a..c925fb708d2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,7 +1,5 @@ package com.sap.sailing.gwt.ui.client.shared.charts; -import java.util.UUID; - import com.google.gwt.user.client.ui.Button; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; @@ -43,7 +41,7 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart Date: Mon, 5 Feb 2018 15:02:10 +0100 Subject: [PATCH 094/214] Bug 4380: Added JavaDocs --- .../client/shared/race/TrackedRaceCreationResultDialog.java | 5 +++++ .../sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/race/TrackedRaceCreationResultDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/race/TrackedRaceCreationResultDialog.java index f76bdf46711..e9bebd55f04 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/race/TrackedRaceCreationResultDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/race/TrackedRaceCreationResultDialog.java @@ -11,6 +11,11 @@ import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.dialog.DataEntryDialog; +/** + * Summary dialog to be shown after actions that generate a new TrackedRace. + * the dialog shows a link to the RaceBoard for the generated TrackedRace. + * In addition, if available a link to the associated event is shown. + */ public class TrackedRaceCreationResultDialog extends DataEntryDialog { private final VerticalPanel verticalPanel; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java index c6d26e2b138..1e35d095559 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java @@ -6,6 +6,10 @@ import java.util.Set; import com.google.gwt.user.client.rpc.IsSerializable; import com.sap.sse.common.Util; +/** + * DTO that carries data that is needed to show the race slicing UI. + * It contains a proposed race name and a list of already used race names. + */ public class SliceRacePreperationDTO implements IsSerializable { private String proposedRaceName; From e187731b1944217b9575240e25e0007b807a5698 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 15:05:42 +0100 Subject: [PATCH 095/214] Bug 4380: improved comment of a unit test --- .../src/com/sap/sailing/gwt/ui/test/SlicingTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java index 12211daa36b..e723676be92 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java @@ -83,8 +83,8 @@ public class SlicingTest { @Test @Ignore /** - * Currently non functional test case for race slicing. Should be finished when SailingServiceImplMock ist fixed to - * work properly for test cases. + * Currently non functional test case for race slicing. Should be finished when SailingServiceImplMock is fixed to + * work properly for test cases. See bug 4418 for details. */ public void testSliceRace() throws Exception { final LinkedHashMap seriesCreationParameters = new LinkedHashMap<>(); From ba9aadeb17c4a251a818ccbd0ffccf27a570eb4b Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 16:15:56 +0100 Subject: [PATCH 096/214] Bug 4380: Added server-side permission check --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index e83f5ff8d21..e2f26985030 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7028,6 +7028,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo) { + SecurityUtils.getSubject().checkPermission(Permission.MANAGE_TRACKED_RACES.getStringPermissionForObjects(Mode.UPDATE)); if (!canSliceRace(raceIdentifier)) { throw new RuntimeException("Can not slice race"); } From cc247f7978487303aa0f8b0ed25e97441994fc36 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 16:52:46 +0100 Subject: [PATCH 097/214] Bug 4380: Added JavaDocs --- .../gwt/ui/client/shared/charts/ChartZoomChangedEvent.java | 3 +++ .../gwt/ui/client/shared/charts/ChartZoomResetEvent.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java index f784fbeca73..96bfbbd0feb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java @@ -5,6 +5,9 @@ import java.util.Date; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; +/** + * Event to be fired by {@link AbstractRaceChart} when a user zooms into the chart. + */ public class ChartZoomChangedEvent extends GwtEvent{ public static final Type TYPE = new Type<>(); private final Date rangeStart; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java index 6fb5330f082..6db77247c85 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java @@ -3,6 +3,9 @@ package com.sap.sailing.gwt.ui.client.shared.charts; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; +/** + * Event to be fired by {@link AbstractRaceChart} when a user resets the zoom to show the whole time range again. + */ public class ChartZoomResetEvent extends GwtEvent { public static final Type TYPE = new Type<>(); From 9d79e0aff017d6dbed4c655566b79d4d936ea429 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 5 Feb 2018 16:55:08 +0100 Subject: [PATCH 098/214] Bug 4380: Code formatting and removed unnecessary semicolons --- .../shared/charts/ChartZoomChangedEvent.java | 17 +++++++++-------- .../shared/charts/ChartZoomResetEvent.java | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java index 96bfbbd0feb..e3a8cbe9885 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomChangedEvent.java @@ -8,14 +8,15 @@ import com.google.gwt.event.shared.GwtEvent; /** * Event to be fired by {@link AbstractRaceChart} when a user zooms into the chart. */ -public class ChartZoomChangedEvent extends GwtEvent{ +public class ChartZoomChangedEvent extends GwtEvent { public static final Type TYPE = new Type<>(); private final Date rangeStart; private final Date rangeEnd; - public interface Handler extends EventHandler { + + public interface Handler extends EventHandler { void handleZoomChanged(ChartZoomChangedEvent cze); } - + public ChartZoomChangedEvent(Date rangeStart, Date rangeEnd) { this.rangeStart = rangeStart; this.rangeEnd = rangeEnd; @@ -24,17 +25,17 @@ public class ChartZoomChangedEvent extends GwtEvent getAssociatedType() { + public Type getAssociatedType() { return TYPE; } - + protected void dispatch(Handler handler) { handler.handleZoomChanged(this); - }; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java index 6db77247c85..2d610be33bd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartZoomResetEvent.java @@ -20,5 +20,5 @@ public class ChartZoomResetEvent extends GwtEvent { protected void dispatch(Handler handler) { handler.handleZoomReset(this); - }; + } } From c9d546d6468b96269f434b1adfd25e8756565a15 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 6 Feb 2018 08:20:48 +0100 Subject: [PATCH 099/214] Bug 4380: Fix for the slicing UI if a user zooms into the wind chart --- .../sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index a58582be22f..0eda11e1710 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -259,12 +259,14 @@ public abstract class AbstractRaceChart extends A protected void resetMinMaxAndExtremesInterval(boolean redraw) { changeMinMaxAndExtremesInterval(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), redraw); + fireEvent(new ChartZoomResetEvent()); } @Override public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { changeMinMaxAndExtremesInterval(zoomStartTimepoint, zoomEndTimepoint, true); chart.showResetZoom(); // Patched method + fireEvent(new ChartZoomChangedEvent(zoomStartTimepoint, zoomEndTimepoint)); } @Override From 9c8505eb1760c1351d75e0d2e5160ef6583c177a Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 6 Feb 2018 16:08:57 +0100 Subject: [PATCH 100/214] bug1301 Adjusted CSS styles of TabPanel extension section --- .../gwt/common/client/controls/tabbar/TabPanel.ui.xml | 11 +++++++---- .../home/desktop/partials/sailorinfo/SailorInfo.java | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml index 858071eb1ed..62b09ea44e3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml @@ -22,8 +22,13 @@ /* font-size: 0; */ margin-bottom: -1px; } + .tabExtension { float: right; + margin-top: 0.5em + } + + .tabExtension > * { margin-left: 0.5em; } @@ -33,12 +38,10 @@
      - +
      - + \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java index e8f8256c259..c42baa0063c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java @@ -25,7 +25,6 @@ public class SailorInfo extends Composite { sailorInfoLabel.getElement().setInnerHTML(i18n.sailorInfoLongText().replace("\n", "
      ")); Style labelStyle = sailorInfoLabel.getElement().getStyle(); labelStyle.setDisplay(Display.INLINE_BLOCK); - sailorInfoLabel.getElement().getStyle().setMarginTop(0.5, Unit.EM); sailorInfoLabel.getElement().getStyle().setMarginRight(0.5, Unit.EM); panel.add(sailorInfoLabel); From ceac9fa27484e6458ff6cb0a1fa6d80ff38402fd Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 6 Feb 2018 16:10:19 +0100 Subject: [PATCH 101/214] bug1301 Added first version of Windfinder launch pad --- .../partials/windfinder/Windfinder.gss | 59 +++++++++++++ .../windfinder/WindfinderControl.java | 51 +++++++++++ .../windfinder/WindfinderControl.ui.xml | 10 +++ .../windfinder/WindfinderLaunchPad.java | 43 ++++++++++ .../WindfinderLaunchPadController.java | 81 ++++++++++++++++++ .../windfinder/WindfinderLaunchPadItem.java | 26 ++++++ .../windfinder/WindfinderLaunchPadItem.ui.xml | 10 +++ .../windfinder/WindfinderResources.java | 33 +++++++ .../partials/windfinder/windfinder-logo.png | Bin 0 -> 854 bytes 9 files changed, 313 insertions(+) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo.png diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss new file mode 100644 index 00000000000..586cbbb24c4 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss @@ -0,0 +1,59 @@ +.windfindercontrol { + background-color: #d0021b; + text-transform: uppercase; + text-decoration: none; + vertical-align: bottom; + padding-left: 1em; + padding-right: 0.5em +} +.windfindercontrol_text, .windfindercontrol_icon { + display: inline-block; +} +.windfindercontrol_icon { + margin: -0.5em 0.25em; +} +.windfindercontrol:hover { + cursor: pointer; + background-color: #ba011a; +} +.windfinderlaunchpad_content { + display: block; + bottom: 0; + right: 0; + box-shadow: 0px 0px 0.666666666666667em 0px rgba(0,0,0,0.2); +} +.windfinderlaunchpad_content_item { + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -moz-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 3em; + padding: 0 1.333333333333333em 0 0.666666666666667em; + background: #fff; + color: #d0021b; + font-weight: normal; +} +.windfinderlaunchpad_content_item:hover { + background-color: #d0021b; + color: #ffffff; + text-decoration: underline; +} +.windfinderlaunchpad_content_item_icon { + margin-right: 1em; +} +.windfinderlaunchpad_content_item_title { + white-space: nowrap; +} +.windfindercontrol_icon, .windfinderlaunchpad_content_item_icon { + width: 1.666666666666667em; + height: 1.666666666666667em; + background-image: resourceUrl("windfinderLogo"); + background-size: contain; + background-repeat: no-repeat; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java new file mode 100644 index 00000000000..0a776dc86dd --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java @@ -0,0 +1,51 @@ +package com.sap.sailing.gwt.home.desktop.partials.windfinder; + +import java.util.function.BiFunction; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sse.common.TimePoint; + +public class WindfinderControl extends Widget { + + interface WindfinderLaunchPadItemUiBinder extends UiBinder { + } + + private static WindfinderLaunchPadItemUiBinder uiBinder = GWT.create(WindfinderLaunchPadItemUiBinder.class); + + @UiField + DivElement labelUi; + private final WindfinderLaunchPadController launchPadController; + private Iterable spots; + + public WindfinderControl(Iterable spots, BiFunction windfinderUrlFactory) { + WindfinderResources.INSTANCE.css().ensureInjected(); + this.launchPadController = new WindfinderLaunchPadController(windfinderUrlFactory); + this.spots = spots; + setElement(uiBinder.createAndBindUi(this)); + sinkEvents(Event.ONCLICK); + } + + @Override + public void onBrowserEvent(Event event) { + // Only handle click events + if (event.getTypeInt() != Event.ONCLICK) { + return; + } + + // If rendered as direct link button, open link in new tab directly instead of showing the menu popup + if (launchPadController.renderAsDirectLink(spots)) { + Window.open(launchPadController.getDirectLinkUrl(spots), "_blank", ""); + return; + } + + launchPadController.showWindfinderLaunchPad(spots, this.getElement()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml new file mode 100644 index 00000000000..6e640043efc --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml @@ -0,0 +1,10 @@ + + + + + +
      +
      Weather Data
      +
      +
      +
      \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java new file mode 100644 index 00000000000..7bc82d227f3 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java @@ -0,0 +1,43 @@ +package com.sap.sailing.gwt.home.desktop.partials.windfinder; + +import java.util.function.BiFunction; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +class WindfinderLaunchPad extends Widget { + + private final DivElement container = Document.get().createDivElement(); + private final BiFunction windfinderUrlFactory; + private final PopupPanel parent; + + WindfinderLaunchPad(Iterable windfinderSpots, + BiFunction windfinderUrlFactory, PopupPanel parent) { + this.container.addClassName(WindfinderResources.INSTANCE.css().windfinderlaunchpad_content()); + this.windfinderUrlFactory = windfinderUrlFactory; + this.parent = parent; + windfinderSpots.forEach(this::addItem); + setElement(container); + sinkEvents(Event.ONCLICK); + } + + @Override + public void onBrowserEvent(final Event event) { + if (event.getTypeInt() == Event.ONCLICK) { + parent.hide(); + } + super.onBrowserEvent(event); + } + + private void addItem(SpotDTO windfinderSpot) { + final String url = windfinderUrlFactory.apply(windfinderSpot, MillisecondsTimePoint.now()); + this.container.appendChild(new WindfinderLaunchPadItem(windfinderSpot.getName(), url).getElement()); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java new file mode 100644 index 00000000000..f93b24e1690 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java @@ -0,0 +1,81 @@ +package com.sap.sailing.gwt.home.desktop.partials.windfinder; + +import java.util.Collection; +import java.util.function.BiFunction; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +class WindfinderLaunchPadController { + + private final PopupPanel panel = new PopupPanel(true, false); + private final BiFunction windfinderUrlFactory; + + /** + * Creates a new {@link WindfinderLaunchPadController} instance with the given URL factory. + * + * @param raceviewerUrlFactory + * {@link BiFunction Factory method} to get the windfinder URL for given {@link SpotDTO spot} and + * {@link TimePoint time point} information + */ + WindfinderLaunchPadController(BiFunction windfinderUrlFactory) { + this.windfinderUrlFactory = windfinderUrlFactory; + } + + /** + * Provides the direct link URL for the given {@link SpotDTO spot}. + * + * @param data + * the {@link SpotDTO spot} to get the link URL for + * @return the direct link URL (as {@link String}) + */ + String getDirectLinkUrl(Iterable data) { + assert renderAsDirectLink(data); + final SpotDTO spot = data.iterator().next(); + return windfinderUrlFactory.apply(spot, MillisecondsTimePoint.now()); + } + + /** + * Shows a {@link WindfinderLaunchPad} for the given {@link SpotDTO spot}s relative to the also provided element. + * + * @param data + * {@link Collection} of {@link SpotDTO spot}s to create a launch pad for + * @param relTo + * the {@link Element element} to show the lauOnch pad relative to + */ + void showWindfinderLaunchPad(Iterable data, Element relTo) { + relTo.scrollIntoView(); + panel.setWidget(new WindfinderLaunchPad(data, windfinderUrlFactory, panel)); + panel.setVisible(false); + panel.show(); + Scheduler.get().scheduleDeferred(() -> { + Widget panelContent = panel.getWidget(); + int alignRight = relTo.getAbsoluteLeft() + relTo.getOffsetWidth() - panelContent.getOffsetWidth(); + int left = (alignRight - Window.getScrollLeft() < 0 ? relTo.getAbsoluteLeft() - 1 : alignRight + 1); + int alignBottom = relTo.getAbsoluteTop() + relTo.getOffsetHeight() - panelContent.getOffsetHeight(); + int top = (alignBottom - Window.getScrollTop() < 0 ? relTo.getAbsoluteTop() - 1 : alignBottom + 1); + panel.setPopupPosition(left, top); + panel.setVisible(true); + }); + } + + /** + * Determine whether a direct link should be/is rendered for the given {@link SpotDTO spot}s instead of a menu + * popup. + * + * @param data + * {@link Collection} of the {@link SpotDTO spot}s which should be/are rendered + * @return true if a direct link should be/is rendered, false otherwise + */ + boolean renderAsDirectLink(Iterable data) { + return Util.size(data) == 1; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java new file mode 100644 index 00000000000..d2490318a43 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java @@ -0,0 +1,26 @@ +package com.sap.sailing.gwt.home.desktop.partials.windfinder; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.AnchorElement; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.UIObject; + +class WindfinderLaunchPadItem extends UIObject { + + private static WindfinderLaunchPadItemUiBinder uiBinder = GWT.create(WindfinderLaunchPadItemUiBinder.class); + + interface WindfinderLaunchPadItemUiBinder extends UiBinder { + } + + @UiField DivElement itemLabelUi; + private final AnchorElement anchorUi; + + WindfinderLaunchPadItem(String label, String raceViewerUrl) { + setElement(anchorUi = uiBinder.createAndBindUi(this)); + anchorUi.setHref(raceViewerUrl); + itemLabelUi.setInnerText(label); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml new file mode 100644 index 00000000000..e59f8ca330c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml @@ -0,0 +1,10 @@ + + + + + + +
      +
      +
      +
      \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java new file mode 100644 index 00000000000..ebcf2b2baba --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java @@ -0,0 +1,33 @@ +package com.sap.sailing.gwt.home.desktop.partials.windfinder; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.ImageResource; + +public interface WindfinderResources extends ClientBundle { + public static final WindfinderResources INSTANCE = GWT.create(WindfinderResources.class); + + @Source("Windfinder.gss") + LocalCss css(); + + @Source("windfinder-logo.png") + ImageResource windfinderLogo(); + + public interface LocalCss extends CssResource { + + String windfindercontrol(); + + String windfindercontrol_icon(); + + String windfindercontrol_text(); + + String windfinderlaunchpad_content(); + + String windfinderlaunchpad_content_item(); + + String windfinderlaunchpad_content_item_icon(); + + String windfinderlaunchpad_content_item_title(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo.png b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..854063763a2f8ca3eb3bf3869017d7c7e92f7db5 GIT binary patch literal 854 zcmV-c1F8IpP)pF8FWQhbW?9;ba!ELWdLwtX>N2bZe?^J zG%heMF*(%MvSa`N0@F!EK~!i%-I|GZn=lYX{r`X1B}vnyY4$xao1hv+P>u8u5@0~S z=Uk6@)=22Y$2N9$P178;FihRz>bfZ(pXA-+++d43wjWGc8Bh09gdCz9Yc_4(^6o(vKlBPJ>3Y}%8#ffz#}2k=d3!HQ6s(^eY|t^V zIoQF*j3)bWN`=kC4z{4!?`0W{GoKTz>{OgNonS@hz?s(xmbQF&lw~@#5LBG`onT4z zNzDaZ1BMGZH)xI3Hh2ioI;(6jl%05e(|3bFYc03I;_^zCDB9jo*m|unoZFDA02BR} zGv^4w9N!7UIjvq^Wr@a~Cj`4k3B$P*pP#af!k){EelN9qlrWq_^j%2=e6IHo%6zc# zrZAl9guh%1o3FgNqs#{zZwkXHijRq#t17y+=N-Zx-wDI9R>M!F5YXI1pdKX*$5`Ia z^PyRS<{tv}C}9|Dets#7AN@v;`a3H9dxB{ZFt>#Dn8I*`KG1k(_5XtadOQTwr-Wgo zn7XE0G#)D6-euug9sX9s`b=RMA*SczGe+_JB1=?{`CJcQBTp4s@vu`IV2%zi9&wi^QlCIwth68$`uiEv? zeGGh!vZO2q-fsv)kB4IFTBu(M!yXR_T8|6`^(tX#JK`6fF~GH@A)r1b41ouyyuPJ^ z)(*Ksfqf(lEf*vE?*2N|svqkE_K+}at>V76n+1LK4$nu6K;x7!G+d41qUY)Zf5>ph zX$=ge z4fZ*EwS(25FN^nImsj>VP0~C#7<7T0U;LrF=6Z{09*jagbv&OV*H+!oN%qnH?{lic g@R)?*@!0JC0Z|%Rncwr7)Bpeg07*qoM6N<$g4dLeY5)KL literal 0 HcmV?d00001 From 83f2fc732921b82a76140f357ebacca8423bbfc4 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 6 Feb 2018 16:23:56 +0100 Subject: [PATCH 102/214] bug1301 Reverted CSS styles adjustments for TabPanel extension section --- .../sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml | 1 - .../sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml index 62b09ea44e3..15379e2d27b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/controls/tabbar/TabPanel.ui.xml @@ -25,7 +25,6 @@ .tabExtension { float: right; - margin-top: 0.5em } .tabExtension > * { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java index c42baa0063c..e8f8256c259 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/sailorinfo/SailorInfo.java @@ -25,6 +25,7 @@ public class SailorInfo extends Composite { sailorInfoLabel.getElement().setInnerHTML(i18n.sailorInfoLongText().replace("\n", "
      ")); Style labelStyle = sailorInfoLabel.getElement().getStyle(); labelStyle.setDisplay(Display.INLINE_BLOCK); + sailorInfoLabel.getElement().getStyle().setMarginTop(0.5, Unit.EM); sailorInfoLabel.getElement().getStyle().setMarginRight(0.5, Unit.EM); panel.add(sailorInfoLabel); From f8ab8316244b6e1c3faa7ce6dfd4887ad583f703 Mon Sep 17 00:00:00 2001 From: "service.tip.git" Date: Wed, 7 Feb 2018 16:06:54 +0000 Subject: [PATCH 103/214] [INTERNAL] Translation delivery: commit by SLS Change-Id: Ifa61db0c06024f1f14d0d2862be5990b4efe8e72 --- .../com/sap/sailing/gwt/ui/client/StringMessages_es.properties | 1 + .../com/sap/sailing/gwt/ui/client/StringMessages_fr.properties | 1 + .../com/sap/sailing/gwt/ui/client/StringMessages_ja.properties | 1 + .../com/sap/sailing/gwt/ui/client/StringMessages_pt.properties | 3 ++- .../com/sap/sailing/gwt/ui/client/StringMessages_ru.properties | 1 + .../com/sap/sailing/gwt/ui/client/StringMessages_zh.properties | 1 + 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties index 2bf2bdf7e05..dfb1d686fff 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=Abrir Raceboard importFinishedGotoEvent=Abrir evento chooseAName=Seleccione un nombre unexpectedErrorDuringFileImport=Se ha producido un error inesperado durante la carga del fichero. +couldNotDownload=No se ha podido descargar el fichero: {0} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index 4ea3990fe20..e8909559ced 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=Ouvrir tableau des courses importFinishedGotoEvent=Ouvrir manifestation chooseAName=Indiquer un nom unexpectedErrorDuringFileImport=Une erreur inattendue s''est produite lors du chargement du fichier. +couldNotDownload=Impossible de télécharger le fichier : {0} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index 0ab94d6477d..0339841644e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=レースボードを開く importFinishedGotoEvent=イベントを開く chooseAName=名称を選択 unexpectedErrorDuringFileImport=ファイルのアップロード時に予期しないエラーが発生しました。 +couldNotDownload={0} ファイルをダウンロードできませんでした。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties index 0f76cd0e86b..25a3076c7f4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties @@ -1687,7 +1687,7 @@ raceAnalysis=Análise da corrida startAnalysis=Análise da partida winningLanes=Rotas vencedoras maximumNumberOfDiscards=Número máximo de descartes -externalLinks=LInks externos +externalLinks=Links externos urlForAllLanguages=URL para todos os idiomas urlForLanguage=URL para idioma ''{0}'' notificationRegattaConfigurationUpdatedUsingByMarks=A configuração de regata ativa foi definida, utilizando o designer do percurso "Por marcas". @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=Abrir painel de corridas importFinishedGotoEvent=Abrir evento chooseAName=Selecionar um nome unexpectedErrorDuringFileImport=Ocorreu um erro inesperado durante o carregamento do arquivo. +couldNotDownload=Não foi possível baixar o arquivo: {0} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 497b3fec68d..6c344a82fed 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=Открыть таблицу гонки importFinishedGotoEvent=Открыть событие chooseAName=Выберите имя unexpectedErrorDuringFileImport=При отправке файла возникла непредвиденная ошибка. +couldNotDownload=Не удалось загрузить файл: {0} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 4bad8be6a87..a577f65b50f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -2033,3 +2033,4 @@ importFinishedGotoRaceboard=打开比赛板 importFinishedGotoEvent=打开活动 chooseAName=选择名称 unexpectedErrorDuringFileImport=文件上传期间发生意外错误。 +couldNotDownload=无法下载文件:{0} From 6ce2e148a871dffa906e7d0f5c2dfad357095bce Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Thu, 8 Feb 2018 08:20:41 +0100 Subject: [PATCH 104/214] Bug 4380: added release notes regarding race slicing --- java/com.sap.sailing.www/release_notes_admin.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index 6883aba4ca2..300064dbd93 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -28,6 +28,14 @@
    • In the raceboard the floating video box was improved, leading to a more solid resizing and moving behavior.
    • In the raceboard, the adding of videos has now an additional fallback startDate, if no startOfRace could be determined, and the file did not contain a proper creationtime tag, startOfTracking is used.
    • In the raceboard, the floating video box will now only display an edit button in the header, next to close and minimize. Upon clicking it, the box will expand and show the prior always shown editing buttons. This saves a lot of screenspace for logged in admin enabled users. +
    • For smartphone tracked races it's now possible to slice a new race from an existing one in the RaceBoard. + As a user having sufficient permissions you need to select a specific time range in the competitor chart of a race. + This will make a new Button appear near to the settings button of the chart. + When you click this button you are being asked for a name of the new race. + A new race column is created in the same series the current race is associated to. + A new tracked race is then associated to the new column for the fleet of the current race. + This new race is being filled with the data from the current race for the selected time range. + After the slicing process is finished, a dialog will open providing a link to the RaceBoard.

    December 2017

    From 4531808f24b1496638f383aafd81b4fea9437075 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Thu, 8 Feb 2018 08:23:31 +0100 Subject: [PATCH 105/214] Bug 4380: improved release notes regarding race slicing --- java/com.sap.sailing.www/release_notes_admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index 300064dbd93..d458b50cd27 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -35,7 +35,7 @@ A new race column is created in the same series the current race is associated to. A new tracked race is then associated to the new column for the fleet of the current race. This new race is being filled with the data from the current race for the selected time range. - After the slicing process is finished, a dialog will open providing a link to the RaceBoard. + After the slicing process is finished, a dialog will open providing a link to the RaceBoard for the newly added race.

December 2017

From 9ad7b48ff574d2edd356cc7fced88094be1b7f3b Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 11:59:30 +0100 Subject: [PATCH 106/214] bug1301 Improved version of Windfinder launch pad --- .../desktop/partials/windfinder/Windfinder.gss | 12 ++++++++---- .../partials/windfinder/WindfinderResources.java | 7 +++++-- .../partials/windfinder/windfinder-logo-red.png | Bin 0 -> 787 bytes ...dfinder-logo.png => windfinder-logo-white.png} | Bin 4 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-red.png rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/{windfinder-logo.png => windfinder-logo-white.png} (100%) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss index 586cbbb24c4..029931cb74c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss @@ -11,6 +11,7 @@ } .windfindercontrol_icon { margin: -0.5em 0.25em; + background-image: resourceUrl("windfinderLogoWhite"); } .windfindercontrol:hover { cursor: pointer; @@ -44,16 +45,19 @@ color: #ffffff; text-decoration: underline; } -.windfinderlaunchpad_content_item_icon { +.windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_icon { margin-right: 1em; + background-image: resourceUrl("windfinderLogoRed"); } -.windfinderlaunchpad_content_item_title { +.windfinderlaunchpad_content_item:hover .windfinderlaunchpad_content_item_icon { + background-image: resourceUrl("windfinderLogoWhite"); +} +.windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_title { white-space: nowrap; } -.windfindercontrol_icon, .windfinderlaunchpad_content_item_icon { +.windfindercontrol_icon, .windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_icon { width: 1.666666666666667em; height: 1.666666666666667em; - background-image: resourceUrl("windfinderLogo"); background-size: contain; background-repeat: no-repeat; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java index ebcf2b2baba..9bcf31f3cab 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java @@ -11,8 +11,11 @@ public interface WindfinderResources extends ClientBundle { @Source("Windfinder.gss") LocalCss css(); - @Source("windfinder-logo.png") - ImageResource windfinderLogo(); + @Source("windfinder-logo-white.png") + ImageResource windfinderLogoWhite(); + + @Source("windfinder-logo-red.png") + ImageResource windfinderLogoRed(); public interface LocalCss extends CssResource { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-red.png b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-red.png new file mode 100644 index 0000000000000000000000000000000000000000..d9c3d0a7075a7542e6919a7a2bcbb45204d8186b GIT binary patch literal 787 zcmV+u1MK{XP)N2bZe?^J zG%heMF*(%MvSa`N0+2~WK~!i%-Iv>O!yphvGif9L=_OUUhBT1>bd!#hdo`>E(8}5c zguovs)&rJ%yjYIkmSuS<{@y=6!6Wdg4w{u9_XC!|LmfC9L5v-X<#;@I2Ppt@G2y{6 z>cr7O_-A6moPr3Rf6g@4?aA?Kkr82FaMm?p?H<_`8W9AxbS-8MK?L=5P1v?4*%ciH zh74gfiDzuxBT3OwK*&-#GOi%Pm2AnJF$EE^k;Yu~^#ufHG`t=W?7xMzM}K`dA!$9d}$5w8^0#^m%9#9}paWDCpV?A{T51hG)9 z9PtWn9naZasDX=w#k3F}yn<@?h Date: Thu, 8 Feb 2018 12:00:30 +0100 Subject: [PATCH 107/214] bug1301 Integration of Windfinder launch pad in home (desktop) --- .../TabletAndDesktopMultiRegattaEventView.java | 7 +++++++ .../event/regatta/TabletAndDesktopRegattaEventView.java | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java index ba2e614c7fd..a7be4ff3473 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java @@ -8,13 +8,16 @@ import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanel; import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanelPlaceSelectionEvent; import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; import com.sap.sailing.gwt.home.desktop.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.desktop.partials.sailorinfo.SailorInfo; +import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Util; public class TabletAndDesktopMultiRegattaEventView extends Composite implements EventMultiregattaView { @@ -46,6 +49,10 @@ public class TabletAndDesktopMultiRegattaEventView extends Composite implements if(sailorsInfoURL != null && ! sailorsInfoURL.isEmpty()) { tabPanelUi.addTabExtension(new SailorInfo(sailorsInfoURL)); } + final Iterable spots = currentPresenter.getEventDTO().getAllWindFinderSpotIdsUsedByEvent(); + if (spots != null && !Util.isEmpty(spots)) { + tabPanelUi.addTabExtension(new WindfinderControl(spots, SpotDTO::getCurrentlyMostAppropriateUrl)); + } } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java index 65bdfbdd1ba..4b7a6e8b11e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java @@ -15,12 +15,14 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.dto.EventType; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.common.client.SharedResources; import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanel; import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanelPlaceSelectionEvent; import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; import com.sap.sailing.gwt.home.desktop.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.desktop.partials.sailorinfo.SailorInfo; +import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.desktop.places.event.regatta.overviewtab.RegattaOverviewTabView; import com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab.RegattaRacesTabView; import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper; @@ -28,6 +30,7 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace; import com.sap.sailing.gwt.ui.client.FlagImageResolver; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Util; import com.sap.sse.gwt.client.LinkUtil; public class TabletAndDesktopRegattaEventView extends Composite implements EventRegattaView { @@ -88,6 +91,11 @@ public class TabletAndDesktopRegattaEventView extends Composite implements Event tabPanelUi.addTabExtension(new SailorInfo(sailorsInfoURL)); } } + + final Iterable spots = currentPresenter.getEventDTO().getAllWindFinderSpotIdsUsedByEvent(); + if (spots != null && !Util.isEmpty(spots)) { + tabPanelUi.addTabExtension(new WindfinderControl(spots, SpotDTO::getCurrentlyMostAppropriateUrl)); + } } @Override From 3196a9500f4384c8939ce43f12258a02e709239a Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 12:01:03 +0100 Subject: [PATCH 108/214] bug1301 First integration of Windfinder launch pad in home (mobile) --- .../mobile/places/event/AbstractEventActivity.java | 8 ++++++++ .../mobile/places/event/AbstractEventView.java | 11 ++++++++++- .../mobile/places/event/AbstractEventView.ui.xml | 14 ++++++++++++-- .../home/mobile/places/event/EventViewBase.java | 5 +++++ .../multiregatta/MultiRegattaActivity.java | 1 + .../event/overview/regatta/RegattaActivity.java | 2 ++ 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java index cbf4b79f802..483bb412fba 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java @@ -14,6 +14,7 @@ import com.google.gwt.event.shared.EventBus; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.EventType; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.event.EventState; @@ -91,6 +92,13 @@ public abstract class AbstractEventActivity ex } } + protected final void initWindfinderNavigations(EventViewBase view) { + final Iterable spots = eventDTO.getAllWindFinderSpotIdsUsedByEvent(); + if (spots != null && !Util.isEmpty(spots)) { + view.setWindfinderNavigations(spots, SpotDTO::getCurrentlyMostAppropriateUrl); + } + } + protected final void initQuickfinder(EventViewBase view, boolean showQuickfinder) { EventViewDTO event = eventDTO; if(showQuickfinder && event.getType() == EventType.MULTI_REGATTA) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java index b511df1d3bc..54bd3ed9d67 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.BiFunction; import com.google.gwt.core.client.GWT; import com.google.gwt.safehtml.shared.SafeHtmlUtils; @@ -13,9 +14,11 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO; +import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.mobile.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.mobile.partials.quickfinder.Quickfinder; import com.sap.sailing.gwt.home.mobile.partials.section.MobileSection; @@ -26,6 +29,7 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.refresh.RefreshManager; import com.sap.sailing.gwt.home.shared.refresh.RefreshManagerWithErrorAndBusy; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.TimePoint; public abstract class AbstractEventView

extends Composite implements EventViewBase { @@ -38,7 +42,7 @@ public abstract class AbstractEventView

exten @UiField(provided = true) EventHeader eventHeaderUi; @UiField Quickfinder quickFinderUi; @UiField SimpleInfoBlock simpleInfoUi; - @UiField SimplePanel viewContentUi; + @UiField SimplePanel viewContentUi, windfinderContainerUi; private AbstractEventViewLayout(EventViewDTO event, String regattaName, PlaceNavigation logoNavigation) { this.eventHeaderUi = new EventHeader(event, regattaName, logoNavigation); @@ -125,6 +129,11 @@ public abstract class AbstractEventView

exten layout.simpleInfoUi.setAction(buttonLabel, url); } + @Override + public void setWindfinderNavigations(Iterable spots, BiFunction urlFactory) { + layout.windfinderContainerUi.setWidget(new WindfinderControl(spots, urlFactory)); + } + @Override public void setSeriesNavigation(String buttonLabel, PlaceNavigation placeNavigation) { layout.simpleInfoUi.setAction(buttonLabel, placeNavigation); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml index 04b1b458666..ae46ced5625 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml @@ -7,13 +7,23 @@ background-color: #f2f2f2; padding-bottom: 10px; } + .windfinder { + padding: 0 10px; + } + .windfinder > div { + border-radius: .3846154em; + width: 100%; + text-align: center; + margin-top: 10px; + }

- + +
- + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java index bbfdfa56783..5d26e34f628 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java @@ -3,8 +3,10 @@ package com.sap.sailing.gwt.home.mobile.places.event; import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import com.google.gwt.user.client.ui.IsWidget; +import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; @@ -20,6 +22,7 @@ import com.sap.sailing.gwt.home.mobile.places.SeriesLeaderboardNavigationProvide import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.places.event.EventContext; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; +import com.sap.sse.common.TimePoint; public interface EventViewBase extends IsWidget { @@ -31,6 +34,8 @@ public interface EventViewBase extends IsWidget { void setSailorInfos(String description, String buttonLabel, String url); + void setWindfinderNavigations(Iterable spots, BiFunction urlFactory); + void setSeriesNavigation(String buttonLabel, PlaceNavigation placeNavigation); public interface Presenter extends RegattaLeaderboardNavigationProvider, SeriesLeaderboardNavigationProvider, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/multiregatta/MultiRegattaActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/multiregatta/MultiRegattaActivity.java index 0242247e4c9..e091431a82b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/multiregatta/MultiRegattaActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/multiregatta/MultiRegattaActivity.java @@ -24,6 +24,7 @@ public class MultiRegattaActivity extends AbstractEventActivity final AbstractEventOverview view = new RegattaOverviewImpl(this, flagImageResolver); initSeriesNavigation(view); initQuickfinder(view, true); + initWindfinderNavigations(view); if (!isMultiRegattaEvent()) { + // initSailorInfo(view); initMedia(view); } return view; From 39686913da897ee2c7d43dd84818b7cc834928e1 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 13:50:55 +0100 Subject: [PATCH 109/214] bug1301 Enabled WindfinderControl to be initialized first while data is provided afterwards --- .../windfinder/WindfinderControl.java | 22 ++++++++++++++----- .../places/event/AbstractEventActivity.java | 2 +- .../places/event/AbstractEventView.java | 10 ++++----- .../places/event/AbstractEventView.ui.xml | 6 +++-- .../mobile/places/event/EventViewBase.java | 4 +--- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java index 0a776dc86dd..d26c00a802d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java @@ -23,14 +23,24 @@ public class WindfinderControl extends Widget { @UiField DivElement labelUi; private final WindfinderLaunchPadController launchPadController; - private Iterable spots; + private Iterable spotData; - public WindfinderControl(Iterable spots, BiFunction windfinderUrlFactory) { + public WindfinderControl(Iterable spotData, BiFunction windfinderUrlFactory) { + this(windfinderUrlFactory); + this.setSpotData(spotData); + } + + public WindfinderControl(BiFunction windfinderUrlFactory) { WindfinderResources.INSTANCE.css().ensureInjected(); this.launchPadController = new WindfinderLaunchPadController(windfinderUrlFactory); - this.spots = spots; setElement(uiBinder.createAndBindUi(this)); sinkEvents(Event.ONCLICK); + this.setVisible(false); + } + + public void setSpotData(Iterable spotData) { + this.spotData = spotData; + this.setVisible(true); } @Override @@ -41,11 +51,11 @@ public class WindfinderControl extends Widget { } // If rendered as direct link button, open link in new tab directly instead of showing the menu popup - if (launchPadController.renderAsDirectLink(spots)) { - Window.open(launchPadController.getDirectLinkUrl(spots), "_blank", ""); + if (launchPadController.renderAsDirectLink(spotData)) { + Window.open(launchPadController.getDirectLinkUrl(spotData), "_blank", ""); return; } - launchPadController.showWindfinderLaunchPad(spots, this.getElement()); + launchPadController.showWindfinderLaunchPad(spotData, this.getElement()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java index 483bb412fba..0ac77756ff0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java @@ -95,7 +95,7 @@ public abstract class AbstractEventActivity ex protected final void initWindfinderNavigations(EventViewBase view) { final Iterable spots = eventDTO.getAllWindFinderSpotIdsUsedByEvent(); if (spots != null && !Util.isEmpty(spots)) { - view.setWindfinderNavigations(spots, SpotDTO::getCurrentlyMostAppropriateUrl); + view.setWindfinderNavigations(spots); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java index 54bd3ed9d67..3c67d8490ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java @@ -4,7 +4,6 @@ import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.BiFunction; import com.google.gwt.core.client.GWT; import com.google.gwt.safehtml.shared.SafeHtmlUtils; @@ -29,7 +28,6 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.refresh.RefreshManager; import com.sap.sailing.gwt.home.shared.refresh.RefreshManagerWithErrorAndBusy; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.common.TimePoint; public abstract class AbstractEventView

extends Composite implements EventViewBase { @@ -42,10 +40,12 @@ public abstract class AbstractEventView

exten @UiField(provided = true) EventHeader eventHeaderUi; @UiField Quickfinder quickFinderUi; @UiField SimpleInfoBlock simpleInfoUi; - @UiField SimplePanel viewContentUi, windfinderContainerUi; + @UiField(provided = true) WindfinderControl windfinderUi; + @UiField SimplePanel viewContentUi;; private AbstractEventViewLayout(EventViewDTO event, String regattaName, PlaceNavigation logoNavigation) { this.eventHeaderUi = new EventHeader(event, regattaName, logoNavigation); + this.windfinderUi = new WindfinderControl(SpotDTO::getCurrentlyMostAppropriateUrl); } } @@ -130,8 +130,8 @@ public abstract class AbstractEventView

exten } @Override - public void setWindfinderNavigations(Iterable spots, BiFunction urlFactory) { - layout.windfinderContainerUi.setWidget(new WindfinderControl(spots, urlFactory)); + public void setWindfinderNavigations(Iterable spotData) { + layout.windfinderUi.setSpotData(spotData); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml index ae46ced5625..787f387b398 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml @@ -1,6 +1,8 @@ + xmlns:dp="urn:import:com.sap.sailing.gwt.home.desktop.partials" + xmlns:sp="urn:import:com.sap.sailing.gwt.home.shared.partials" + xmlns:mp="urn:import:com.sap.sailing.gwt.home.mobile.partials"> .headerWrapper { @@ -22,7 +24,7 @@ - +

diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java index 5d26e34f628..4d156277716 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/EventViewBase.java @@ -3,7 +3,6 @@ package com.sap.sailing.gwt.home.mobile.places.event; import java.util.Collection; import java.util.Map; import java.util.Set; -import java.util.function.BiFunction; import com.google.gwt.user.client.ui.IsWidget; import com.sap.sailing.domain.common.windfinder.SpotDTO; @@ -22,7 +21,6 @@ import com.sap.sailing.gwt.home.mobile.places.SeriesLeaderboardNavigationProvide import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.places.event.EventContext; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; -import com.sap.sse.common.TimePoint; public interface EventViewBase extends IsWidget { @@ -34,7 +32,7 @@ public interface EventViewBase extends IsWidget { void setSailorInfos(String description, String buttonLabel, String url); - void setWindfinderNavigations(Iterable spots, BiFunction urlFactory); + void setWindfinderNavigations(Iterable spotData); void setSeriesNavigation(String buttonLabel, PlaceNavigation placeNavigation); From d13b8acef382b1820c7e1bce2e4bdab961bb2399 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 14:05:15 +0100 Subject: [PATCH 110/214] bug1301 Minor styling adjustment for mobile event views header section --- .../partials/simpleinfoblock/SimpleInfoBlock.java | 4 ++++ .../partials/simpleinfoblock/SimpleInfoBlock.ui.xml | 4 ++-- .../home/mobile/places/event/AbstractEventView.ui.xml | 10 +++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.java index e231411c309..7f70e6fbfe4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.java @@ -25,21 +25,25 @@ public class SimpleInfoBlock extends Composite { public SimpleInfoBlock() { initWidget(uiBinder.createAndBindUi(this)); + getElement().getStyle().setDisplay(Display.NONE); } public void setDescription(SafeHtml description) { descriptionUi.getStyle().setDisplay(Display.BLOCK); + getElement().getStyle().clearDisplay(); descriptionUi.setInnerSafeHtml(description); } public void setAction(String label, String url) { actionUi.getStyle().setDisplay(Display.BLOCK); + getElement().getStyle().clearDisplay(); actionUi.setInnerText(label); actionUi.setHref(UriUtils.fromString(url)); } public void setAction(String label, PlaceNavigation placeNavigation) { actionUi.getStyle().setDisplay(Display.BLOCK); + getElement().getStyle().clearDisplay(); actionUi.setInnerText(label); placeNavigation.configureAnchorElement(actionUi); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml index eb8d58a8434..1324d141a51 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml @@ -7,7 +7,7 @@ .description { display: none; font-size: .866666666666667em; - margin-bottom: .769230769230769em; + margin-bottom: .384615384615385em; } .action { display: none; @@ -17,7 +17,7 @@ } .holder { box-sizing: border-box; - padding: 0px 10px 0px 10px; + padding: 0px 10px 10px; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml index 787f387b398..88ec30aad76 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml @@ -7,16 +7,12 @@ .headerWrapper { background-color: #f2f2f2; - padding-bottom: 10px; } .windfinder { - padding: 0 10px; - } - .windfinder > div { - border-radius: .3846154em; - width: 100%; + display: block; + margin: 0 10px 10px; + border-radius: .384615384615385em; text-align: center; - margin-top: 10px; } From d90950ccd1e6ad16938663fa66aed87b0d0591f7 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 15:12:49 +0100 Subject: [PATCH 111/214] bug1301 Added abstraction for launch pad controller classes --- .../RaceviewerLaunchPad.java | 13 +-- .../RaceviewerLaunchPadCell.java | 2 +- .../RaceviewerLaunchPadController.java | 80 ++----------------- .../windfinder/WindfinderControl.java | 5 +- .../WindfinderLaunchPadController.java | 61 +++----------- .../AbstractLaunchPadController.java | 51 ++++++++++++ .../launchpad/LaunchPadController.java | 41 ++++++++++ .../AbstractRegattaCompetitionFleetRace.java | 2 +- 8 files changed, 120 insertions(+), 135 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/AbstractLaunchPadController.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/LaunchPadController.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPad.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPad.java index cafa6ccb351..f6a4160658f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPad.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPad.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.home.desktop.partials.raceviewerlaunchpad; +import java.util.function.BiFunction; + import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; import com.google.gwt.uibinder.client.UiBinder; @@ -12,7 +14,7 @@ import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.raceboard.RaceBoardModes; -public abstract class RaceviewerLaunchPad extends Composite { +class RaceviewerLaunchPad extends Composite { private static RaceviewerLaunchPadUiBinder uiBinder = GWT.create(RaceviewerLaunchPadUiBinder.class); @@ -21,9 +23,12 @@ public abstract class RaceviewerLaunchPad extends Composite { @UiField RaceviewerLaunchPadResources local_res; @UiField DivElement itemContainerUi; + private final BiFunction raceboardUrlFactory; private final PopupPanel parent; - public RaceviewerLaunchPad(SimpleRaceMetadataDTO data, final PopupPanel parent) { + RaceviewerLaunchPad(SimpleRaceMetadataDTO data, + BiFunction raceboardUrlFactory, PopupPanel parent) { + this.raceboardUrlFactory = raceboardUrlFactory; this.parent = parent; initWidget(uiBinder.createAndBindUi(this)); local_res.css().ensureInjected(); @@ -50,7 +55,7 @@ public abstract class RaceviewerLaunchPad extends Composite { } private void addItem(SimpleRaceMetadataDTO data, RaceviewerLaunchPadMenuItem item) { - String raceViewerUrl = getRaceViewerURL(data, item.raceBoardMode); + String raceViewerUrl = raceboardUrlFactory.apply(data, item.raceBoardMode); itemContainerUi.appendChild(new RaceviewerLaunchPadItem(item.label, item.icon, raceViewerUrl).getElement()); } @@ -70,6 +75,4 @@ public abstract class RaceviewerLaunchPad extends Composite { } } - protected abstract String getRaceViewerURL(SimpleRaceMetadataDTO data, String raceBoardMode); - } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadCell.java index 3dcc9d69ca6..5fc73b513e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadCell.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadCell.java @@ -55,7 +55,7 @@ public class RaceviewerLaunchPadCell> extends Abstr if (!launchPadController.renderAsDirectLink(data)) { if (data.hasValidTrackingData() && BrowserEvents.CLICK.equals(event.getType()) && parent.getFirstChildElement().isOrHasChild(Element.as(event.getEventTarget()))) { - launchPadController.showRaceviewerLaunchPad(data, parent); + launchPadController.showLaunchPad(data, parent); } return; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadController.java index 436f0199468..cd2e11927ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/raceviewerlaunchpad/RaceviewerLaunchPadController.java @@ -2,21 +2,16 @@ package com.sap.sailing.gwt.home.desktop.partials.raceviewerlaunchpad; import java.util.function.BiFunction; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.dom.client.Element; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.PopupPanel; -import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO; +import com.sap.sailing.gwt.home.shared.partials.launchpad.AbstractLaunchPadController; import com.sap.sailing.gwt.ui.raceboard.RaceBoardModes; -public class RaceviewerLaunchPadController { +public class RaceviewerLaunchPadController extends AbstractLaunchPadController { public static enum RenderingStyle { WATCH_LIVE_ONLY, WATCH_LIVE_OR_ANALYZE, ANALYZE, NOT_TRACKED } - private final PopupPanel panel = new PopupPanel(true, false); private final BiFunction raceboardUrlFactory; /** @@ -27,78 +22,17 @@ public class RaceviewerLaunchPadController { * race} and {@link RaceBoardModes raceboard mode} information */ public RaceviewerLaunchPadController(BiFunction raceboardUrlFactory) { + super((data, panel) -> new RaceviewerLaunchPad(data, raceboardUrlFactory, panel)); this.raceboardUrlFactory = raceboardUrlFactory; } - /** - * Provides the direct link URL for the given {@link SimpleRaceMetadataDTO race}. - * - * @param data - * the {@link SimpleRaceMetadataDTO race} to get the link URL for - * @return the direct link URL (as {@link String}) - */ - public String getDirectLinkUrl(T data) { + @Override + public String getDirectLinkUrl(SimpleRaceMetadataDTO data) { return raceboardUrlFactory.apply(data, RaceBoardModes.PLAYER.name()); } - /** - * Creates a {@link RaceviewerLaunchPad} for the given {@link SimpleRaceMetadataDTO race} and {@link PopupPanel} - * instance. - * - * @param data - * the {@link SimpleRaceMetadataDTO race} to create a launch pad for - * @param parent - * the {@link PopupPanel} which will contain the launch pad - * @return the newly created {@link RaceviewerLaunchPad} instance - */ - public RaceviewerLaunchPad getRaceviewerLaunchPad(T data, PopupPanel parent) { - return new RaceviewerLaunchPad(data, parent) { - @Override - protected String getRaceViewerURL(SimpleRaceMetadataDTO data, String raceBoardMode) { - return raceboardUrlFactory.apply(data, raceBoardMode); - } - }; - } - - /** - * Shows a {@link RaceviewerLaunchPad} for the given {@link SimpleRaceMetadataDTO race} relative to the also - * provided element. - * - * @param data - * the {@link SimpleRaceMetadataDTO race} to create a launch pad for - * @param relTo - * the {@link Element element} to show the launch pad relative to - */ - public void showRaceviewerLaunchPad(T data, Element relTo) { - relTo.scrollIntoView(); - panel.setWidget(new RaceviewerLaunchPad(data, panel) { - @Override - protected String getRaceViewerURL(SimpleRaceMetadataDTO data, String raceBoardMode) { - return raceboardUrlFactory.apply(data, raceBoardMode); - } - }); - panel.setVisible(false); - panel.show(); - Scheduler.get().scheduleDeferred(() -> { - Widget panelContent = panel.getWidget(); - int alignRight = relTo.getAbsoluteLeft() + relTo.getOffsetWidth() - panelContent.getOffsetWidth(); - int left = (alignRight - Window.getScrollLeft() < 0 ? relTo.getAbsoluteLeft() - 1 : alignRight + 1); - int alignBottom = relTo.getAbsoluteTop() + relTo.getOffsetHeight() - panelContent.getOffsetHeight(); - int top = (alignBottom - Window.getScrollTop() < 0 ? relTo.getAbsoluteTop() - 1 : alignBottom + 1); - panel.setPopupPosition(left, top); - panel.setVisible(true); - }); - } - - /** - * Determine whether a direct link should be/is rendered for the given {@link SimpleRaceMetadataDTO race} instead of - * a menu popup. - * - * @param data - * the {@link SimpleRaceMetadataDTO race} which should be/is rendered - * @return true if a direct link should be/is rendered, false otherwise - */ - public boolean renderAsDirectLink(T data) { + @Override + public boolean renderAsDirectLink(SimpleRaceMetadataDTO data) { return !data.isRunning() && !data.isFinished(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java index d26c00a802d..07325e624ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java @@ -20,8 +20,7 @@ public class WindfinderControl extends Widget { private static WindfinderLaunchPadItemUiBinder uiBinder = GWT.create(WindfinderLaunchPadItemUiBinder.class); - @UiField - DivElement labelUi; + @UiField DivElement labelUi; private final WindfinderLaunchPadController launchPadController; private Iterable spotData; @@ -56,6 +55,6 @@ public class WindfinderControl extends Widget { return; } - launchPadController.showWindfinderLaunchPad(spotData, this.getElement()); + launchPadController.showLaunchPad(spotData, this.getElement()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java index f93b24e1690..b660a6b3951 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java @@ -1,80 +1,37 @@ package com.sap.sailing.gwt.home.desktop.partials.windfinder; -import java.util.Collection; import java.util.function.BiFunction; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.dom.client.Element; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.PopupPanel; -import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sailing.gwt.home.shared.partials.launchpad.AbstractLaunchPadController; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; -class WindfinderLaunchPadController { +class WindfinderLaunchPadController extends AbstractLaunchPadController> { - private final PopupPanel panel = new PopupPanel(true, false); private final BiFunction windfinderUrlFactory; /** * Creates a new {@link WindfinderLaunchPadController} instance with the given URL factory. * * @param raceviewerUrlFactory - * {@link BiFunction Factory method} to get the windfinder URL for given {@link SpotDTO spot} and + * {@link BiFunction Factory} to get the windfinder URL for given {@link SpotDTO spot} and * {@link TimePoint time point} information */ - WindfinderLaunchPadController(BiFunction windfinderUrlFactory) { + WindfinderLaunchPadController(final BiFunction windfinderUrlFactory) { + super((data, panel) -> new WindfinderLaunchPad(data, windfinderUrlFactory, panel)); this.windfinderUrlFactory = windfinderUrlFactory; } - /** - * Provides the direct link URL for the given {@link SpotDTO spot}. - * - * @param data - * the {@link SpotDTO spot} to get the link URL for - * @return the direct link URL (as {@link String}) - */ - String getDirectLinkUrl(Iterable data) { - assert renderAsDirectLink(data); + @Override + public String getDirectLinkUrl(Iterable data) { final SpotDTO spot = data.iterator().next(); return windfinderUrlFactory.apply(spot, MillisecondsTimePoint.now()); } - /** - * Shows a {@link WindfinderLaunchPad} for the given {@link SpotDTO spot}s relative to the also provided element. - * - * @param data - * {@link Collection} of {@link SpotDTO spot}s to create a launch pad for - * @param relTo - * the {@link Element element} to show the lauOnch pad relative to - */ - void showWindfinderLaunchPad(Iterable data, Element relTo) { - relTo.scrollIntoView(); - panel.setWidget(new WindfinderLaunchPad(data, windfinderUrlFactory, panel)); - panel.setVisible(false); - panel.show(); - Scheduler.get().scheduleDeferred(() -> { - Widget panelContent = panel.getWidget(); - int alignRight = relTo.getAbsoluteLeft() + relTo.getOffsetWidth() - panelContent.getOffsetWidth(); - int left = (alignRight - Window.getScrollLeft() < 0 ? relTo.getAbsoluteLeft() - 1 : alignRight + 1); - int alignBottom = relTo.getAbsoluteTop() + relTo.getOffsetHeight() - panelContent.getOffsetHeight(); - int top = (alignBottom - Window.getScrollTop() < 0 ? relTo.getAbsoluteTop() - 1 : alignBottom + 1); - panel.setPopupPosition(left, top); - panel.setVisible(true); - }); - } - - /** - * Determine whether a direct link should be/is rendered for the given {@link SpotDTO spot}s instead of a menu - * popup. - * - * @param data - * {@link Collection} of the {@link SpotDTO spot}s which should be/are rendered - * @return true if a direct link should be/is rendered, false otherwise - */ - boolean renderAsDirectLink(Iterable data) { + @Override + public boolean renderAsDirectLink(Iterable data) { return Util.size(data) == 1; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/AbstractLaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/AbstractLaunchPadController.java new file mode 100644 index 00000000000..9496a380162 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/AbstractLaunchPadController.java @@ -0,0 +1,51 @@ +package com.sap.sailing.gwt.home.shared.partials.launchpad; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; + +/** + * Abstract {@link LaunchPadController} implementation providing common functionality for a specific type of data. + * + * @param + * the actual data type + */ +public abstract class AbstractLaunchPadController implements LaunchPadController { + + private final PopupPanel panel = new PopupPanel(true, false); + private final BiFunction launchPadFactory; + + /** + * Creates a new {@link AbstractLaunchPadController} using the provided {@link Function factory} to create the + * {@link Widget launch pad}. + * + * @param launchPadFactory + * the {@link Function factory} to create the launch pad + */ + protected AbstractLaunchPadController(BiFunction launchPadFactory) { + this.launchPadFactory = launchPadFactory; + } + + @Override + public final void showLaunchPad(D data, Element relTo) { + relTo.scrollIntoView(); + panel.setWidget(launchPadFactory.apply(data, panel)); + panel.setVisible(false); + panel.show(); + Scheduler.get().scheduleDeferred(() -> { + Widget panelContent = panel.getWidget(); + int alignRight = relTo.getAbsoluteLeft() + relTo.getOffsetWidth() - panelContent.getOffsetWidth(); + int left = (alignRight - Window.getScrollLeft() < 0 ? relTo.getAbsoluteLeft() - 1 : alignRight + 1); + int alignBottom = relTo.getAbsoluteTop() + relTo.getOffsetHeight() - panelContent.getOffsetHeight(); + int top = (alignBottom - Window.getScrollTop() < 0 ? relTo.getAbsoluteTop() - 1 : alignBottom + 1); + panel.setPopupPosition(left, top); + panel.setVisible(true); + }); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/LaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/LaunchPadController.java new file mode 100644 index 00000000000..5d57d5b1037 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/launchpad/LaunchPadController.java @@ -0,0 +1,41 @@ +package com.sap.sailing.gwt.home.shared.partials.launchpad; + +import com.google.gwt.dom.client.Element; + +/** + * Interface for a launch pad controller class for a specific type of data. + * + * @param + * the actual data type + */ +public interface LaunchPadController { + + /** + * Provides the direct link URL for the given data. + * + * @param data + * the data to get the link URL for + * @return the direct link URL (as {@link String}) + */ + String getDirectLinkUrl(D data); + + /** + * Shows a launch pad for the given data relative to the provided element. + * + * @param data + * the data to create a launch pad for + * @param relTo + * the {@link Element element} to show the launch pad relative to + */ + void showLaunchPad(D data, Element relTo); + + /** + * Determine whether a direct link should be/is rendered for the given data instead of a menu popup. + * + * @param data + * the data which should be/is rendered + * @return true if a direct link should be/is rendered, false otherwise + */ + boolean renderAsDirectLink(D data); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java index c567a4b3429..c75b9104c1f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java @@ -51,7 +51,7 @@ public abstract class AbstractRegattaCompetitionFleetRace extends Widget impleme return; } - launchPadController.showRaceviewerLaunchPad(race, this.getElement()); + launchPadController.showLaunchPad(race, this.getElement()); } private void setupRaceState(SimpleRaceMetadataDTO race) { From 8c29360cb5c122e37ee3f5b8028eddd6e10282e9 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 15:17:05 +0100 Subject: [PATCH 112/214] bug1301 Moved WindFinder components to shared partials package --- .../TabletAndDesktopMultiRegattaEventView.java | 2 +- .../regatta/TabletAndDesktopRegattaEventView.java | 2 +- .../home/mobile/places/event/AbstractEventView.java | 2 +- .../mobile/places/event/AbstractEventView.ui.xml | 3 +-- .../partials/windfinder/Windfinder.gss | 0 .../partials/windfinder/WindfinderControl.java | 2 +- .../partials/windfinder/WindfinderControl.ui.xml | 2 +- .../partials/windfinder/WindfinderLaunchPad.java | 2 +- .../windfinder/WindfinderLaunchPadController.java | 2 +- .../windfinder/WindfinderLaunchPadItem.java | 2 +- .../windfinder/WindfinderLaunchPadItem.ui.xml | 2 +- .../partials/windfinder/WindfinderResources.java | 2 +- .../partials/windfinder/windfinder-logo-red.png | Bin .../partials/windfinder/windfinder-logo-white.png | Bin 14 files changed, 11 insertions(+), 12 deletions(-) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/Windfinder.gss (100%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderControl.java (97%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderControl.ui.xml (84%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderLaunchPad.java (96%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderLaunchPadController.java (95%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderLaunchPadItem.java (93%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderLaunchPadItem.ui.xml (84%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/WindfinderResources.java (93%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/windfinder-logo-red.png (100%) rename java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/{desktop => shared}/partials/windfinder/windfinder-logo-white.png (100%) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java index a7be4ff3473..8068c526841 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/multiregatta/TabletAndDesktopMultiRegattaEventView.java @@ -14,8 +14,8 @@ import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanelPlaceSelectionE import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; import com.sap.sailing.gwt.home.desktop.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.desktop.partials.sailorinfo.SailorInfo; -import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper; +import com.sap.sailing.gwt.home.shared.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Util; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java index 4b7a6e8b11e..2e738b85c8d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/TabletAndDesktopRegattaEventView.java @@ -22,11 +22,11 @@ import com.sap.sailing.gwt.common.client.controls.tabbar.TabPanelPlaceSelectionE import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; import com.sap.sailing.gwt.home.desktop.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.desktop.partials.sailorinfo.SailorInfo; -import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.desktop.places.event.regatta.overviewtab.RegattaOverviewTabView; import com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab.RegattaRacesTabView; import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper; import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; +import com.sap.sailing.gwt.home.shared.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace; import com.sap.sailing.gwt.ui.client.FlagImageResolver; import com.sap.sailing.gwt.ui.client.StringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java index 3c67d8490ea..9f0d24cda4a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.java @@ -17,7 +17,6 @@ import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO; -import com.sap.sailing.gwt.home.desktop.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.mobile.partials.eventheader.EventHeader; import com.sap.sailing.gwt.home.mobile.partials.quickfinder.Quickfinder; import com.sap.sailing.gwt.home.mobile.partials.section.MobileSection; @@ -25,6 +24,7 @@ import com.sap.sailing.gwt.home.mobile.partials.sectionHeader.SectionHeaderConte import com.sap.sailing.gwt.home.mobile.partials.simpleinfoblock.SimpleInfoBlock; import com.sap.sailing.gwt.home.mobile.places.QuickfinderPresenter; import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; +import com.sap.sailing.gwt.home.shared.partials.windfinder.WindfinderControl; import com.sap.sailing.gwt.home.shared.refresh.RefreshManager; import com.sap.sailing.gwt.home.shared.refresh.RefreshManagerWithErrorAndBusy; import com.sap.sailing.gwt.ui.client.StringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml index 88ec30aad76..4d665ebeded 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml @@ -1,6 +1,5 @@ @@ -20,7 +19,7 @@ - +
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss similarity index 100% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/Windfinder.gss rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java similarity index 97% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java index 07325e624ea..fb2e7b95960 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.home.desktop.partials.windfinder; +package com.sap.sailing.gwt.home.shared.partials.windfinder; import java.util.function.BiFunction; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml similarity index 84% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml index 6e640043efc..5ed936a8208 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderControl.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml @@ -2,7 +2,7 @@ - +
Weather Data
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPad.java similarity index 96% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPad.java index 7bc82d227f3..dc0a6dfadca 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPad.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPad.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.home.desktop.partials.windfinder; +package com.sap.sailing.gwt.home.shared.partials.windfinder; import java.util.function.BiFunction; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadController.java similarity index 95% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadController.java index b660a6b3951..8ea12343f6e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadController.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.home.desktop.partials.windfinder; +package com.sap.sailing.gwt.home.shared.partials.windfinder; import java.util.function.BiFunction; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java similarity index 93% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java index d2490318a43..527ce311171 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.home.desktop.partials.windfinder; +package com.sap.sailing.gwt.home.shared.partials.windfinder; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.AnchorElement; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml similarity index 84% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml index e59f8ca330c..90ba87fd341 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderLaunchPadItem.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml @@ -2,7 +2,7 @@ - +
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java similarity index 93% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java index 9bcf31f3cab..c705fb03825 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/WindfinderResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.home.desktop.partials.windfinder; +package com.sap.sailing.gwt.home.shared.partials.windfinder; import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ClientBundle; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-red.png b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-red.png similarity index 100% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-red.png rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-red.png diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-white.png b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-white.png similarity index 100% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/windfinder/windfinder-logo-white.png rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-white.png From 6e91aba127ed313c419395aaa032dca6bdc6d6d3 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 8 Feb 2018 15:19:51 +0100 Subject: [PATCH 113/214] bug1301 Code / markup formatting only --- .../simpleinfoblock/SimpleInfoBlock.ui.xml | 48 +++++++++---------- .../places/event/AbstractEventView.ui.xml | 38 +++++++-------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml index 1324d141a51..8d56f614227 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/simpleinfoblock/SimpleInfoBlock.ui.xml @@ -1,27 +1,25 @@ - - - - .description { - display: none; - font-size: .866666666666667em; - margin-bottom: .384615384615385em; - } - .action { - display: none; - border-radius: .384615384615385em; - width: 100%; - text-align: center; - } - .holder { - box-sizing: border-box; - padding: 0px 10px 10px; - } - - -
-
- + + + + .description { + display: none; + font-size: .866666666666667em; + margin-bottom: .384615384615385em; + } + .action { + display: none; + border-radius: .384615384615385em; + width: 100%; + text-align: center; + } + .holder { + box-sizing: border-box; + padding: 0px 10px 10px; + } + + +
+ +
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml index 4d665ebeded..2258c94f492 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventView.ui.xml @@ -1,26 +1,26 @@ - - - .headerWrapper { - background-color: #f2f2f2; - } - .windfinder { + xmlns:sp="urn:import:com.sap.sailing.gwt.home.shared.partials" + xmlns:mp="urn:import:com.sap.sailing.gwt.home.mobile.partials"> + + + .headerWrapper { + background-color: #f2f2f2; + } + .windfinder { display: block; margin: 0 10px 10px; border-radius: .384615384615385em; text-align: center; - } - - -
- - - - -
- -
+ } +
+ +
+ + + + +
+ +
From 73e7090ca3cac859db804298a63e159b2dd4f831 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 9 Feb 2018 18:19:51 +0100 Subject: [PATCH 114/214] whiteace change only Change-Id: Ibf0f5576f5273f5d0b0660b1ed508d91130f750e --- java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch | 4 ++-- .../java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java | 4 ---- java/com.sap.sse.security.ui/Security UI sdm.launch | 2 +- java/com.sap.sse.security.ui/Security UI.launch | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index 553394cf99f..91d87dbb354 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -6,7 +6,7 @@ - + @@ -61,7 +61,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index b9f68ec579e..93397624f5f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -365,22 +365,18 @@ public class RaceBoardPanel competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); competitorChart.setVisible(false); competitorChart.updateSettings(multiCompetitorRaceChartSettings); - new SliceRaceHandler(sailingService, userService, errorReporter, competitorChart, selectedRaceIdentifier, leaderboardGroupName, leaderboardName, event); - componentsForSideBySideViewer.add(competitorChart); windChart = new WindChart(this, getComponentContext(), windChartLifecycle, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomModel, windChartSettings, stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); - windChart.setVisible(false); windChart.getEntryWidget().setTitle(stringMessages.windChart()); componentsForSideBySideViewer.add(windChart); } - editMarkPassingPanel = new EditMarkPassingsPanel(this, getComponentContext(), sailingService, selectedRaceIdentifier, stringMessages, diff --git a/java/com.sap.sse.security.ui/Security UI sdm.launch b/java/com.sap.sse.security.ui/Security UI sdm.launch index 633ad54780f..db91c43b97e 100644 --- a/java/com.sap.sse.security.ui/Security UI sdm.launch +++ b/java/com.sap.sse.security.ui/Security UI sdm.launch @@ -48,7 +48,7 @@ - + diff --git a/java/com.sap.sse.security.ui/Security UI.launch b/java/com.sap.sse.security.ui/Security UI.launch index 607b86530a3..61ec9497ff6 100644 --- a/java/com.sap.sse.security.ui/Security UI.launch +++ b/java/com.sap.sse.security.ui/Security UI.launch @@ -47,7 +47,7 @@ - + From 6bcf0d5a7e1349207aa47aaa4748d1149e2b9a33 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 10 Feb 2018 04:35:44 +0100 Subject: [PATCH 115/214] Automatically recognize tagged resources from AWS. All resources with tag key 'AutoDiscover' and value 'true' will automatically be discovered. --- configuration/aws-automation/aws-setup.sh | 26 +- .../aws-automation/lib/build-config.sh | 247 ++++++++++++++++++ .../aws-automation/lib/config_eu_west_1.sh | 24 +- .../aws-automation/lib/config_eu_west_2.sh | 3 +- .../aws-automation/lib/functions_ec2.sh | 10 +- .../aws-automation/lib/functions_io.sh | 162 ++++++++++-- .../aws-automation/lib/resources-eu-west-2.sh | 11 + .../lib/scenario_master_instance.sh | 2 +- configuration/aws-automation/lib/utils.sh | 10 +- configuration/aws-automation/lib/variables.sh | 5 +- 10 files changed, 452 insertions(+), 48 deletions(-) create mode 100644 configuration/aws-automation/lib/build-config.sh create mode 100644 configuration/aws-automation/lib/resources-eu-west-2.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index a6b9e5ddd81..ee04c88ac75 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -17,12 +17,6 @@ else exit 1 fi -function source_region_config_file(){ - require_region - local region_config_fle=$(echo lib/config_$region.sh | tr '-' '_') - source $region_config_fle -} - # delete temp files when trapped function trapCleanup() { echo "" @@ -52,10 +46,10 @@ args=() # Create temp directory with three random numbers and the process ID # in the name. This directory is removed automatically at exit. -# tmpDir="./tmp.$RANDOM.$RANDOM.$RANDOM.$$/" -# (umask 000 && mkdir "${tmpDir}") || { - # die "Could not create temporary directory! Exiting." -# } +#tmpDir="/tmp/$RANDOM.$RANDOM.$RANDOM.$$/" +#(umask 000 && mkdir "${tmpDir}") || { +# die "Could not create temporary directory! Exiting." +#} # Logging (not used) # To never save a logfile change variable to '/dev/null' @@ -65,9 +59,19 @@ logFile="$HOME/Library/Logs/${scriptBasename}.log" function mainScript() { echo -n +#require_region +region=eu-west-2 -source_region_config_file +local resource_file=./lib/resources-$region.sh +if is_exists ${resource_file}; then + source "${resource_file}" +fi +init_resources + +require_super_instance_dns_name +echo "SUPER INSTANCE: $super_instance_dns_name" +safeExit if $instance; then instance_start if $associate_alb; then diff --git a/configuration/aws-automation/lib/build-config.sh b/configuration/aws-automation/lib/build-config.sh new file mode 100644 index 00000000000..e20dcf37bf2 --- /dev/null +++ b/configuration/aws-automation/lib/build-config.sh @@ -0,0 +1,247 @@ +#!/bin/bash +# +# https://github.com/brbsix/bash-config +# Read and write config files from Bash + + +readonly PROGRAM="${0##*/}" +readonly VERSION='0.1.dev1' + +readonly SEPARATOR_BASH='=' +readonly SEPARATOR_INI=' = ' + + +error(){ + echo "ERROR: $*" >&2 +} + +get_line_number(){ + set_mode + local output=$(sed -n "/^${parameter}${separator}/=" "$config" 2>/dev/null) + [[ ! -z ${output##*[!0-9]*} ]] && line_number="$output" +} + +main(){ + if echo "$1" | grep -Eq '^(-b|--bash)$'; then + option_mode=bash + elif echo "$1" | grep -Eq '^(-c|--custom)$'; then + option_mode=custom + elif echo "$1" | grep -Eq '^(-i|--ini)$'; then + option_mode=ini + fi + + local args + if [[ $option_mode = bash || $option_mode = ini ]]; then + args=$(($# - 1)) + config=$2 + parameter=$3 + value=$4 + elif [[ $option_mode = custom ]]; then + args=$(($# - 2)) + separator_custom=$2 + config=$3 + parameter=$4 + value=$5 + else + args=$# + config=$1 + parameter=$2 + value=$3 + fi + + if (( args == 0 )); then + error "No arguments" + see_help_msg + exit 1 + elif (( args == 1 )) && echo "$1" | grep -Eq '^(-h|--help|help)$'; then + usage + elif (( args == 1 )) && echo "$1" | grep -Eq '^(-v|--version|version)$'; then + echo "$PROGRAM $VERSION" + exit 0 + elif (( args == 2 )); then + if [[ $parameter = % ]]; then + # DELETE CONFIG + if [[ ! -f $config ]]; then + error "'$config' does not exist" + exit 1 + elif [[ ! -w $config ]]; then + error "Lacking write perms to '$config'" + exit 1 + else + remove_config + fi + else + # READ PARAMETER + if [[ ! -f $config ]]; then + error "'$config' does not exist" + exit 1 + elif [[ ! -r $config ]]; then + error "No read access to '$config'" + exit 1 + else + read_parameter + fi + fi + elif (( args == 3 )); then + if [[ $value = % ]]; then + # DELETE PARAMETER + if [[ ! -f $config ]]; then + error "'$config' does not exist" + exit 1 + elif [[ ! -w $config ]]; then + error "No write access to '$config'" + exit 1 + else + remove_parameter + fi + else + # WRITE PARAMETER + if [[ -f $config && ! -w $config ]]; then + error "No write access to '$config'" + exit 1 + else + write_parameter + fi + fi + else + error "Invalid arguments" + see_help_msg + exit 1 + fi +} + +read_parameter(){ + set_mode + + local output=$(sed -n "s/^${parameter}${separator}//p" "$config" 2>/dev/null) + + if [[ -n $output ]]; then + echo "$output" + else + if grep -q "^${parameter}${separator}$" "$config" 2>/dev/null; then + error "'$parameter' is set to null in '$config'" + exit 0 + else + error "No match for '$parameter' in '$config'" + exit 1 + fi + fi +} + +remove_config(){ + rm -f "$config" &>/dev/null + + if [[ -f $config ]]; then + error "Failed to remove '$config'" + exit 1 + fi +} + +remove_parameter(){ + get_line_number + [[ -n $line_number ]] && sed -i "${line_number}d" "$config" 2>/dev/null + if grep -q "^${parameter}${separator}" "$config" 2>/dev/null; then + error "Failed to remove parameter '$parameter' from '$config'" + exit 1 + elif [[ -f $config && ! -s $config ]]; then # remove config if empty + remove_config + fi +} + +sanitize_parameter(){ + if ! echo "$parameter" | grep -q '^[A-Za-z0-9_-]\+$'; then + error "Parameter may consist of A-Z, a-z, 0-9, underscores, and dashes (no spaces or special characters)" + exit 1 + fi +} + +see_help_msg(){ + echo "Try '$PROGRAM --help' for more information." >&2 +} + +set_mode(){ + grep -q "^[A-Za-z0-9_-]\+${SEPARATOR_BASH}" "$config" &>/dev/null && mode+=(bash) + + grep -q "^[A-Za-z0-9_-]\+${SEPARATOR_INI}" "$config" &>/dev/null && mode+=(ini) + + [[ $option_mode = custom ]] && grep -q "^[A-Za-z0-9_-]\+${separator_custom}" "$config" &>/dev/null && mode+=(custom) + + if (( ${#mode[@]} > 1 )); then + error "Unable to determine a consistent configuration file format. Are you sure '$config' is a config file?" + exit 1 + fi + + # OPTION IS SET + if [[ -n $option_mode ]]; then + + if [[ -n ${mode[0]} && ( ${mode[0]} != $option_mode ) ]]; then + # use detected mode in the event of a conflicting option + echo "WARNING: '$config' was intified as ${mode[0]} mode (ignoring $option_mode option)" >&2 + mode=${mode[0]} + else + # use optioned mode + mode=$option_mode + fi + + # NO OPTION IS SET and we have detected mode + elif [[ -n ${mode[0]} ]]; then + # use detected mode + mode=${mode[0]} + fi + + set_separator "$mode" +} + +set_separator(){ + if [[ $1 = custom ]]; then + separator=$separator_custom + elif [[ $1 = ini ]]; then + separator=$SEPARATOR_INI + else + # default to bash mode + separator=$SEPARATOR_BASH + fi +} + +usage(){ + cat <<-EOF + Usage: $PROGRAM [OPTION] [value] + Read and write config files from bash. + + -b, --bash use bash mode ex. '=' + -c, --custom use custom separator string + -i, --ini use ini mode ex. ' = ' + -v, --version show program's version number and exit + + * Enter a value to create or update an entry + * Specify only a parameter to print the currently set value (if any) + * Use '%' to delete the config file or a specific parameter + EOF + exit 0 +} + +verify_write(){ + local fullstring=${parameter}${separator}${value} + if ! grep -q "^${fullstring}$" "$config" &>/dev/null; then + error "Failed to write '${fullstring}' to '$config'" + exit 1 + fi +} + +write_parameter(){ + if [[ -f $config ]]; then + get_line_number + if [[ -n $line_number ]]; then + sed -i "${line_number}c${parameter}${separator}${value}" "$config" 2>/dev/null + else + echo "${parameter}${separator}${value}" >> "$config" + sort -o "$config" "$config" + fi + else + set_separator "$option_mode" + echo "${parameter}${separator}${value}" >> "$config" + fi + verify_write +} + +main "$@" diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index 00de5919e29..b6cea9aef0f 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -1,19 +1,19 @@ # Variables for region "eu-west-1" (Ireland) +# HTTPS Listener of Application Load Balancer Sailing-eu-west-1 +# listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/Sailing-eu-west-1/32b89dbfe7f75097/f9212223209ac042' + +# Sailing Analytics App +# instance_security_group_id=sg-eaf31e85 + +# SAP Sailing Analytics App HVM 1.61 +# image_id='ami-7719840e' + +# certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' + + default_key_name='Leon' default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' - -# HTTPS Listener of Application Load Balancer Sailing-eu-west-1 -listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/Sailing-eu-west-1/32b89dbfe7f75097/f9212223209ac042' - -# Sailing Analytics App -instance_security_group_ids=sg-eaf31e85 - -# SAP Sailing Analytics App HVM 1.61 -image_id='ami-7719840e' - -certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' -elb_security_group_ids= mongodb_host='dbserver.internal.sapsailing.com' mongodb_port='10202' alb_domain='sapsailing.com' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index a105b6a3f4c..6864a3de592 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -7,13 +7,12 @@ default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' # Sailing Analytics App -instance_security_group_ids=sg-871732ee +instance_security_group_id=sg-871732ee # SAP Sailing Analytics App HVM 1.56 image_id=ami-39f3e25d certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' -elb_security_group_ids=sg-871732ee mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index c3566c366f6..3da0832b574 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -9,6 +9,14 @@ function query_public_dns_name(){ aws_wrapper ec2 describe-instances --instance-ids $1 --output text --query 'Reservations[*].Instances[*].PublicDnsName' } +# ----------------------------------------------------------- +# Get resources that should be automatically discovered +# @return array of resource ids +# ----------------------------------------------------------- +function get_auto_discover_resources(){ + aws_wrapper resourcegroupstaggingapi get-resources --tag-filters "Key=$autoDiscoverResourceTagName,Values=true,True,1,Yes,yes" +} + # ----------------------------------------------------------- # Query for default vpc id # @return default vpc id @@ -85,7 +93,7 @@ function create_instance(){ local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ - --security-group-ids $instance_security_group_ids --user-data "$1" \ + --security-group-ids $instance_security_group_id --user-data "$1" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 4e198512cc5..1edb3304339 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -1,18 +1,41 @@ #!/usr/bin/env bash # ----------------------------------------------------------- -# As long as variable is empty, prompt again. +# Prompt user for input and save value to variable # @param $1 prompt message # @param $2 default value # @param $3 variable that is receiving the value +# @param $4 array of resource names +# @param $5 array of resource arns # ----------------------------------------------------------- -function ask(){ - while [[ -z "${!3}" ]] - do - require_input "$1" "$2" $3 - done +function require_input(){ + declare -a _optionNames=() + declare -a _optionValues=() + if [ ! -z "$4" ] && [ ! -z "$5" ]; then + _optionNames=("${!4}") + _optionValues=("${!5}") + fi + + if [ ${#_optionValues[@]} -gt 1 ]; then + local_echo "--- Names ---" + o=0 + for name in ${optionNames[@]}; do + local_echo "$((++o))) $name" + done + local_echo "--- Resource IDs ---" + select option in ${_optionValues[@]}; + do + read -r $3 <<< $option + break + done + elif [ ${#_optionValues[@]} -eq 1 ]; then + read -r $3 <<< ${_optionValues[0]} + else + read -e -p "$1" -i "$2" $3 + fi } + # ----------------------------------------------------------- # If the needed variable was not passed as a parameter and is not # already set, then prompt the user to input a value and also @@ -22,8 +45,17 @@ function ask(){ # @param $2 variable # @param $3 default value # @param $4 prompt message +# @param $5 array of resource names +# @param $6 array of resource arns # ----------------------------------------------------------- function require_variable(){ + declare -a optionNames=() + declare -a optionValues=() + if [ ! -z "$5" ] && [ ! -z "$6" ]; then + optionNames=("${!5}") + optionValues=("${!6}") + fi + # if parameter is empty if [ -z "$1" ]; then # if required variable is empty @@ -33,8 +65,11 @@ function require_variable(){ # read default value into required variable read -r "$2" <<< "$3" fi - # if force not enabled, ask user for input - ask "$4" "$3" $2 + # if force not enabled, ask user for input and repeat until value is set for variable + while [[ -z "${!2}" ]] + do + require_input "$4" "$3" $2 optionNames[@] optionValues[@] + done fi else # if parameter is not empty, read its value into required variable @@ -50,16 +85,109 @@ function local_echo(){ echo "$@" >&2 } -# ----------------------------------------------------------- -# Prompt user for input and save value to variable -# @param $1 prompt message -# @param $2 default value -# @param $3 variable that is receiving the value -# ----------------------------------------------------------- -function require_input(){ - read -e -p "$1" -i "$2" $3 +function update_configuration(){ + require_region + + mkdir -p ~/.aws-automation + if is_exists ~/.aws-automation/config-$region; then + seek_confirmation "Do you want to update your configuration?" + if is_confirmed; then + rm -rf ~/.aws-automation + write_configuration_to_file + fi + else + local_echo "Creating configuration for region $region in folder ~/.aws-automation..." + write_configuration_to_file + fi } +function select_resource_names(){ + jq -c ".ResourceTagMappingList[] | select(.ResourceARN | contains(\"$1\")) | .Tags[] | select(.Key==\"Name\") | .Value" -r | sanitize +} + +function select_resource_arns(){ + jq -c ".ResourceTagMappingList[] | select(.ResourceARN | contains(\"$1\")) | .ResourceARN" -r | sanitize +} + +function get_names_for_resources(){ + declare -a resources=("${!1}") + declare -a names=() + + for arn in ${resources[@]}; do + name=$(echo "$RESOURCES" | select_resource_names $arn) + if [ -z "$name" ]; then + names+=("[Unnamed]") + else + names+=("$name") + fi + done + echo ${names[@]} +} + +function update_resources(){ + local resources_file=./lib/resources-$region.sh + > $resources_file + + OLD_RESOURCES=$RESOURCES + typeset -p OLD_RESOURCES >> $resources_file + + local filter="security-group" + TAGGED_SECURITY_GROUP_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) + IFS=' ' read -ra TAGGED_SECURITY_GROUP_NAMES <<< "$(get_names_for_resources TAGGED_SECURITY_GROUP_ARNS[@])" + typeset -p TAGGED_SECURITY_GROUP_NAMES TAGGED_SECURITY_GROUP_ARNS >> $resources_file + + local filter="image" + TAGGED_IMAGE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) + IFS=' ' read -ra TAGGED_IMAGE_NAMES <<< "$(get_names_for_resources TAGGED_IMAGE_ARNS[@])" + typeset -p TAGGED_IMAGE_NAMES TAGGED_IMAGE_ARNS >> $resources_file + + local filter="instance" + TAGGED_INSTANCE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) + IFS=' ' read -ra TAGGED_INSTANCE_NAMES <<< "$(get_names_for_resources TAGGED_INSTANCE_ARNS[@])" + typeset -p TAGGED_INSTANCE_NAMES TAGGED_INSTANCE_ARNS >> $resources_file + + local filter="certificate" + TAGGED_CERTIFICATE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) + IFS=' ' read -ra TAGGED_CERTIFICATE_NAMES <<< "$(get_names_for_resources TAGGED_CERTIFICATE_ARNS[@])" + typeset -p TAGGED_CERTIFICATE_NAMES TAGGED_CERTIFICATE_ARNS >> $resources_file + + local filter="loadbalancer" + TAGGED_LOADBALANCER_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) + IFS=' ' read -ra TAGGED_LOADBALANCER_NAMES <<< "$(get_names_for_resources TAGGED_LOADBALANCER_ARNS[@])" + typeset -p TAGGED_LOADBALANCER_NAMES TAGGED_LOADBALANCER_ARNS >> $resources_file + +} +function init_resources(){ + RESOURCES=$(get_auto_discover_resources) + + if [ "$RESOURCES" != "$OLD_RESOURCES" ]; then + update_resources $resources_file + fi + + # for resource in $resources; do + # + # default_key_name='Leon' + # default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' + # + # # through environemnt + # # mongodb_host='dbserver.internal.sapsailing.com' + # # mongodb_port='10202' + # alb_domain='sapsailing.com' + # + # #SL Multi-Instance Sailing Server + # # default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' + # # always ask ssh user + # # default_ssh_user='root' + # + # mkdir ~/.aws-automation + # lib/configurator.sh --bash ~/.aws-automation/config-$region default_key_name $instance_security_group_id + # lib/configurator.sh --bash ~/.aws-automation/config-$region default_key_file $load_balancer + # lib/configurator.sh --bash ~/.aws-automation/config-$region listener_arn $listener_arn + # lib/configurator.sh --bash ~/.aws-automation/config-$region image_id $image_id + # lib/configurator.sh --bash ~/.aws-automation/config-$region certificate_arn $certificate_arn +} + + function require_region(){ require_variable "$region_param" region "$default_region" "$region_ask_message" } @@ -105,7 +233,7 @@ function require_ssh_user(){ } function require_super_instance_dns_name(){ - require_variable "$super_instance_dns_name_param" super_instance_dns_name "$default_super_instance_dns_name" "$super_instance_dns_name_message" + require_variable "$super_instance_dns_name_param" super_instance_dns_name "$default_super_instance_dns_name" "$super_instance_dns_name_message" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] } function require_description(){ diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh new file mode 100644 index 00000000000..cdf8137371d --- /dev/null +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -0,0 +1,11 @@ +declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"test\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"MongoDB\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" +declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="test" [1]="[Unnamed]") +declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff" [1]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c") +declare -a TAGGED_IMAGE_NAMES=([0]="[Unnamed]") +declare -a TAGGED_IMAGE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d") +declare -a TAGGED_INSTANCE_NAMES=([0]="MongoDB") +declare -a TAGGED_INSTANCE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb") +declare -a TAGGED_CERTIFICATE_NAMES=([0]="[Unnamed]") +declare -a TAGGED_CERTIFICATE_ARNS=([0]="arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d") +declare -a TAGGED_LOADBALANCER_NAMES=([0]="[Unnamed]") +declare -a TAGGED_LOADBALANCER_ARNS=([0]="arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a") diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index 186bfdace56..41e3a6b7434 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -63,7 +63,7 @@ function master_instance_execute() { local base64_user_data=$(echo "$user_data_replica" | base64 -w 0) local launch_template_for_replica=$(printf '{"UserData":"%s","ImageId":"%s","InstanceType":"%s","TagSpecifications":[{"ResourceType":"instance","Tags":[{"Key":"Name","Value":"%s"}]}], "SecurityGroupIds": ["%s"], "KeyName": "%s"}'\ - "${base64_user_data}" "$image_id" "$instance_type" "$instance_name (Replica)" "$instance_security_group_ids" "$key_name") + "${base64_user_data}" "$image_id" "$instance_type" "$instance_name (Replica)" "$instance_security_group_id" "$key_name") aws_wrapper ec2 create-launch-template --launch-template-name "Replica_$instance_short_name" --version-description "Replica for $instance_name." --launch-template-data $launch_template_for_replica diff --git a/configuration/aws-automation/lib/utils.sh b/configuration/aws-automation/lib/utils.sh index e8fb75bcfac..d0e53877efe 100644 --- a/configuration/aws-automation/lib/utils.sh +++ b/configuration/aws-automation/lib/utils.sh @@ -82,13 +82,19 @@ fi for utility_file in "${SOURCEPATH}"/*.sh do if [ -e "${utility_file}" ]; then + # Don't source self if [[ "${utility_file}" == *"utils.sh"* ]]; then continue fi - # Don't source region config files (desired file already sourced) - if [[ "${utility_file}" == *"config_"* ]]; then + # Don't source configurator + if [[ "${utility_file}" == *"build-config.sh"* ]]; then + continue + fi + + # Don't source resources file (will be sourced later) + if [[ "${utility_file}" == *"resources"* ]]; then continue fi diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index a7f4ecf018b..1c00d5049e2 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -# START EDIT default_region=eu-west-2 default_instance_type=t2.medium default_server_startup_notify=leon.radeck@sap.com @@ -8,9 +7,10 @@ default_new_admin_password=admin default_user_username=newuser default_user_password=newpassword latest_release=$(get_latest_release) -# END EDIT # Other Variables (DONT EDIT) + +autoDiscoverResourceTagName='AutoDiscover' scriptName=`basename $0` #Set Script Name variable scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 @@ -44,3 +44,4 @@ default_telnet_port='14900' default_expedition_port='2000' default_contact_person='Unknown' default_contact_email='Unkown' +resources_file=./lib/resources-$region.sh From a47a586bfde47b411b8efe40b10734de62b09ed5 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 10 Feb 2018 05:11:36 +0100 Subject: [PATCH 116/214] Timeout, different changes. --- configuration/aws-automation/aws-setup.sh | 5 +-- .../aws-automation/lib/config_eu_west_1.sh | 2 +- .../aws-automation/lib/config_eu_west_2.sh | 9 ++-- .../aws-automation/lib/functions_ec2.sh | 43 +++++++++++-------- .../aws-automation/lib/functions_elb.sh | 8 ++++ .../aws-automation/lib/functions_io.sh | 12 ++++++ .../aws-automation/lib/functions_wrapper.sh | 19 +++++++- .../aws-automation/lib/scenario_instance.sh | 1 + .../lib/scenario_shared_instance.sh | 22 +++++----- 9 files changed, 83 insertions(+), 38 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index ee04c88ac75..f95d82decdf 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -59,8 +59,7 @@ logFile="$HOME/Library/Logs/${scriptBasename}.log" function mainScript() { echo -n -#require_region -region=eu-west-2 +require_region local resource_file=./lib/resources-$region.sh if is_exists ${resource_file}; then @@ -69,8 +68,6 @@ fi init_resources -require_super_instance_dns_name -echo "SUPER INSTANCE: $super_instance_dns_name" safeExit if $instance; then instance_start diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh index b6cea9aef0f..4e7be0a1b68 100644 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ b/configuration/aws-automation/lib/config_eu_west_1.sh @@ -19,5 +19,5 @@ mongodb_port='10202' alb_domain='sapsailing.com' #SL Multi-Instance Sailing Server -default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' +# default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' default_ssh_user='root' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 6864a3de592..56d171d0077 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -4,18 +4,17 @@ default_key_name='leonradeck-keypair' default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' # HTTPS Listener of Application Load Balancer DummyALB -listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' +# listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' # Sailing Analytics App -instance_security_group_id=sg-871732ee +# instance_security_group_id=sg-871732ee # SAP Sailing Analytics App HVM 1.56 -image_id=ami-39f3e25d +#image_id=ami-39f3e25d -certificate_arn='arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d' mongodb_host=35.176.143.232 mongodb_port=27017 alb_domain='dummy2.sapsailing.com' -default_super_instance_dns_name='' +#default_super_instance_dns_name='' default_ssh_user="root" diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 3da0832b574..e0bdd70f100 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -35,6 +35,15 @@ function get_instance_id(){ aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text } +# ----------------------------------------------------------- +# Returns the instance id part of e.g. arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb +# param $1 resource arn +# @return instance id +# ----------------------------------------------------------- +function get_instance_id_of_resource_arn(){ + cut -d/ -f2 <<< $1 +} + function get_public_ip(){ local_echo "Querying for the public ip of $1..." aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicIpAddress=='$1'].InstanceId" --output text @@ -85,22 +94,31 @@ function wait_for_ssh_connection(){ do_until_true ssh_prewrapper -q $1@$2 true 1>&2 } +function run_instance(){ + aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ + --security-group-ids $instance_security_group_id --user-data "$1" \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } +} + + +# ----------------------------------------------------------- +# Get user data of instance with specific instance id +# @param $1 instance id +# @return user data of instance +# ----------------------------------------------------------- +function get_user_data_from_instance() { + aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize | base64 --decode +} + # ----------------------------------------------------------- # Create instance and output response # @param $1 user data # ----------------------------------------------------------- function create_instance(){ local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" - - json_instance=$(aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ - --security-group-ids $instance_security_group_id --user-data "$1" \ - --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } - + json_instance=$(run_instance) instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') - - # wait till instance is recognized by aws wait_instance_exists $instance_id - description=$(aws --region $region ec2 describe-instances --instance-ids $instance_id \ --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ --output table) @@ -111,12 +129,3 @@ function create_instance(){ echo $instance_id } - -# ----------------------------------------------------------- -# Get user data of instance with specific instance id -# @param $1 instance id -# @return user data of instance -# ----------------------------------------------------------- -function get_user_data_from_instance() { - aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize | base64 --decode -} diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 14ffd7f0393..056abe5403f 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -50,6 +50,14 @@ function register_targets(){ aws_wrapper elbv2 register-targets --target-group-arn $1 --targets Id=$2 } +# ----------------------------------------------------------- +# Returns first https listener of load balancer +# @param $1 load balancer arn +# @return https listener arn +# ----------------------------------------------------------- +function get_first_https_listener(){ + aws_wrapper elbv2 describe-listeners --load-balancer-arn $1 --query "Listeners[?Protocol==`HTTPS`].ListenerArn" --output text +} # ----------------------------------------------------------- # Create rule for listener # @param $1 listener arn diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 1edb3304339..ff044d6c8d0 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -187,6 +187,16 @@ function init_resources(){ # lib/configurator.sh --bash ~/.aws-automation/config-$region certificate_arn $certificate_arn } +function require_image_id(){ + require_variable "" image_id "" "$image_id_ask_message" TAGGED_IMAGE_NAMES[@] TAGGED_IMAGE_ARNS[@] +} + +function require_instance_security_group_id(){ + require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" TAGGED_SECURITY_GROUP_NAMES[@] TAGGED_SECURITY_GROUP_ARNS[@] +} +function require_load_balancer(){ + require_variable "" load_balancer "" "$load_balancer_ask_message" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] +} function require_region(){ require_variable "$region_param" region "$default_region" "$region_ask_message" @@ -252,6 +262,8 @@ function require_build_version(){ require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" } +instance_security_group_id_ask_message="Please select the security group for the instance: " +load_balancer_ask_message="Please select the load balancer: " region_ask_message="Please enter the region for the instance: " instance_type_ask_message="Please enter the instance type: " key_name_ask_message="Please enter the name of your keypair to connect to the instance: " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 565829f659e..dc4b779dee0 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -69,8 +69,11 @@ function ssh_wrapper(){ # Executes curl command until status code is 200 # ----------------------------------------------------------- function curl_until_http_200(){ + start_time="$(date -u +%s)" while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "200" ]]; do + fail_on_timeout $start_time + local_echo -n "." sleep $http_retry_interval; done @@ -82,18 +85,32 @@ function curl_until_http_200(){ # TODO: Fix code duplication (curl_until_http_200, curl_until_http_401) # ----------------------------------------------------------- function curl_until_http_401(){ + start_time="$(date -u +%s)" while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "401" ]]; do - local_echo -n "." + fail_on_timeout $start_time + + local_echo -n "." sleep $http_retry_interval; done local_echo "" } function do_until_true(){ + start_time="$(date -u +%s)" until "$@" do + fail_on_timeout $start_time + echo -n . sleep $http_retry_interval done } + +function fail_on_timeout(){ + end_time="$(date -u +%s)" + if [ "$(($end_time-$1))" -gt 180 ]; then + error "TIMEOUT" + safeExit + fi +} diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 2082b639eab..96670383c56 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -16,6 +16,7 @@ function instance_start(){ function instance_require(){ require_region require_instance_type + require_instance_security_group_id require_instance_name require_instance_short_name require_ssh_user diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 16aa01c69c3..989f78117d2 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -15,8 +15,9 @@ function shared_instance_start(){ # the user will be prompted to enter a value # ----------------------------------------------------------- function shared_instance_require(){ - require_super_instance_dns_name + require_super_instance require_ssh_user + require_load_balancer require_key_file require_instance_name @@ -41,7 +42,7 @@ function shared_instance_check_preconditions(){ echo "Checking if directory $server_dir exists already..." execute_remote "[ ! -d $server_dir ]" - echo "Checking if ssh connection $ssh_user@$super_instance_dns_name is working..." + echo "Checking if ssh connection $ssh_user@$super_instance is working..." execute_remote ls } @@ -103,29 +104,30 @@ function shared_instance_execute() { header "Event and user creation" # get access token - access_token=$(get_access_token $admin_username $admin_password $super_instance_dns_name $server_port) + access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) # create event - event_id=$(create_event $access_token $super_instance_dns_name $server_port $instance_name) + event_id=$(create_event $access_token $super_instance $server_port $instance_name) # change admin password - change_admin_password $access_token $super_instance_dns_name $server_port $admin_username $new_admin_password + change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password # create new user - user=$(create_new_user $access_token $super_instance_dns_name $server_port $user_username $user_password) + user=$(create_new_user $access_token $super_instance $server_port $user_username $user_password) header "Configuring ALB" + listener_arn=$(get_first_https_listener $load_balancer) local target_group_arn=$(create_target_group "S-shared-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "$server_port" "5" "4" "2" "2" - register_targets $target_group_arn $(get_instance_id $super_instance_dns_name) + register_targets $target_group_arn $(get_instance_id_of_resource_arn $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) header "Configuring Apache" - append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance_dns_name $server_port + append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port local_echo "Reloading httpd..." out=$(execute_remote "/etc/init.d/httpd reload") @@ -153,9 +155,9 @@ function find_first_unused_port(){ } function execute_remote(){ - ssh_wrapper $ssh_user@$super_instance_dns_name "$@" + ssh_wrapper $ssh_user@$super_instance "$@" } function get_last_used_port(){ - ssh_prewrapper $ssh_user@$super_instance_dns_name "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" + ssh_prewrapper $ssh_user@$super_instance "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" } From d47ab08dd384b13568e71c5d8d45534081d71118 Mon Sep 17 00:00:00 2001 From: Radeck Date: Sat, 10 Feb 2018 04:13:27 +0000 Subject: [PATCH 117/214] Removed alb domain setting --- configuration/aws-automation/lib/config_eu_west_2.sh | 1 - configuration/aws-automation/lib/functions_elb.sh | 2 +- configuration/aws-automation/lib/scenario_instance.sh | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh index 56d171d0077..e2acb1ad257 100644 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ b/configuration/aws-automation/lib/config_eu_west_2.sh @@ -14,7 +14,6 @@ default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' mongodb_host=35.176.143.232 mongodb_port=27017 -alb_domain='dummy2.sapsailing.com' #default_super_instance_dns_name='' default_ssh_user="root" diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 056abe5403f..54de7a6110b 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -69,7 +69,7 @@ function create_rule(){ local_echo "Creating rule for listener..." local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.$alb_domain" + local domain="$subdomain.sapsailing.com rule=$(aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3) echo $domain diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 96670383c56..944ed1fe7d6 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -17,6 +17,7 @@ function instance_require(){ require_region require_instance_type require_instance_security_group_id + require_image_id require_instance_name require_instance_short_name require_ssh_user From 053ec77b09fd9958f9cc0c8b9156f537934ec0c4 Mon Sep 17 00:00:00 2001 From: Radeck Date: Sat, 10 Feb 2018 05:21:41 +0000 Subject: [PATCH 118/214] Configuration ~/.aws-automation/config- file for personal settings --- configuration/aws-automation/aws-setup.sh | 1 - .../aws-automation/lib/functions_ec2.sh | 14 ++-- .../aws-automation/lib/functions_elb.sh | 4 +- .../aws-automation/lib/functions_io.sh | 73 +++++++++---------- .../aws-automation/lib/resources-eu-west-2.sh | 10 +-- .../lib/scenario_shared_instance.sh | 2 +- 6 files changed, 50 insertions(+), 54 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index f95d82decdf..318455b99e2 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -67,7 +67,6 @@ if is_exists ${resource_file}; then fi init_resources - safeExit if $instance; then instance_start diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index e0bdd70f100..2887ede1200 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -40,10 +40,14 @@ function get_instance_id(){ # param $1 resource arn # @return instance id # ----------------------------------------------------------- -function get_instance_id_of_resource_arn(){ +function get_resource_id(){ cut -d/ -f2 <<< $1 } +function is_resource_arn(){ + [[ $a == "arn:*" ]] +} + function get_public_ip(){ local_echo "Querying for the public ip of $1..." aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicIpAddress=='$1'].InstanceId" --output text @@ -95,9 +99,9 @@ function wait_for_ssh_connection(){ } function run_instance(){ - aws_wrapper ec2 run-instances --image-id $image_id --count $instance_count --instance-type $instance_type --key-name $key_name \ - --security-group-ids $instance_security_group_id --user-data "$1" \ - --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]") || { safeExit; } + aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count $instance_count --instance-type $instance_type --key-name $key_name \ + --security-group-ids $(get_resource_id $instance_security_group_id) --user-data "$1" \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" || { safeExit; } } @@ -116,7 +120,7 @@ function get_user_data_from_instance() { # ----------------------------------------------------------- function create_instance(){ local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" - json_instance=$(run_instance) + json_instance=$(run_instance "$1") instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') wait_instance_exists $instance_id description=$(aws --region $region ec2 describe-instances --instance-ids $instance_id \ diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 54de7a6110b..0d8d9b5a16e 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -56,7 +56,7 @@ function register_targets(){ # @return https listener arn # ----------------------------------------------------------- function get_first_https_listener(){ - aws_wrapper elbv2 describe-listeners --load-balancer-arn $1 --query "Listeners[?Protocol==`HTTPS`].ListenerArn" --output text + aws_wrapper elbv2 describe-listeners --load-balancer-arn $1 --query "Listeners[?Protocol=='HTTPS'].ListenerArn" --output text } # ----------------------------------------------------------- # Create rule for listener @@ -69,7 +69,7 @@ function create_rule(){ local_echo "Creating rule for listener..." local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.sapsailing.com + local domain="$subdomain.sapsailing.com" rule=$(aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3) echo $domain diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index ff044d6c8d0..0a5a196c321 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -85,22 +85,6 @@ function local_echo(){ echo "$@" >&2 } -function update_configuration(){ - require_region - - mkdir -p ~/.aws-automation - if is_exists ~/.aws-automation/config-$region; then - seek_confirmation "Do you want to update your configuration?" - if is_confirmed; then - rm -rf ~/.aws-automation - write_configuration_to_file - fi - else - local_echo "Creating configuration for region $region in folder ~/.aws-automation..." - write_configuration_to_file - fi -} - function select_resource_names(){ jq -c ".ResourceTagMappingList[] | select(.ResourceARN | contains(\"$1\")) | .Tags[] | select(.Key==\"Name\") | .Value" -r | sanitize } @@ -164,27 +148,32 @@ function init_resources(){ update_resources $resources_file fi - # for resource in $resources; do - # - # default_key_name='Leon' - # default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' - # - # # through environemnt - # # mongodb_host='dbserver.internal.sapsailing.com' - # # mongodb_port='10202' - # alb_domain='sapsailing.com' - # - # #SL Multi-Instance Sailing Server - # # default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' - # # always ask ssh user - # # default_ssh_user='root' - # - # mkdir ~/.aws-automation - # lib/configurator.sh --bash ~/.aws-automation/config-$region default_key_name $instance_security_group_id - # lib/configurator.sh --bash ~/.aws-automation/config-$region default_key_file $load_balancer - # lib/configurator.sh --bash ~/.aws-automation/config-$region listener_arn $listener_arn - # lib/configurator.sh --bash ~/.aws-automation/config-$region image_id $image_id - # lib/configurator.sh --bash ~/.aws-automation/config-$region certificate_arn $certificate_arn + update_configuration +} + +function update_configuration(){ + mkdir -p ~/.aws-automation + touch ~/.aws-automation/config-$region + + init_config_variable default_key_name + init_config_variable default_key_file +} + +function init_config_variable(){ + local tmp=$(get_config_variable "$1") + if [ -z $tmp ]; then + set_config_variable "$1" "" + else + read -r "$1" <<< $tmp + fi +} + +function set_config_variable(){ + ./lib/build-config.sh --bash ~/.aws-automation/config-$region "$1" "$2" +} + +function get_config_variable(){ + ./lib/build-config.sh --bash ~/.aws-automation/config-$region "$1" } function require_image_id(){ @@ -242,8 +231,12 @@ function require_ssh_user(){ require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" } -function require_super_instance_dns_name(){ - require_variable "$super_instance_dns_name_param" super_instance_dns_name "$default_super_instance_dns_name" "$super_instance_dns_name_message" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] +function require_super_instance(){ + require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] + if is_resource_arn $super_instance; then + local instance_id=$(get_resource_id $super_instance) + super_instance=$(query_public_dns_name $instance_id) + fi } function require_description(){ @@ -277,7 +270,7 @@ user_username_ask_message="Please enter the username of your new user: " user_password_ask_message="Please enter the password of your new user: " public_dns_name_ask_message="Please enter the public dns name: " ssh_user_ask_message="Please enter the ssh user: " -super_instance_dns_name_message="Please enter the dns name of the superior instance: " +super_instance_message="Please enter the dns name of the superior instance: " description_message="Please enter a description for the server: " contact_person_message="Please enter the name of a contact person: " contact_email_message="Please enter the email of the contact person: " diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh index cdf8137371d..ca6b3765edb 100644 --- a/configuration/aws-automation/lib/resources-eu-west-2.sh +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -1,10 +1,10 @@ -declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"test\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"MongoDB\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" -declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="test" [1]="[Unnamed]") -declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff" [1]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c") +declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"appSecurityGroup\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" +declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="appSecurityGroup" [1]="[Unnamed]") +declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee" [1]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c") declare -a TAGGED_IMAGE_NAMES=([0]="[Unnamed]") declare -a TAGGED_IMAGE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d") -declare -a TAGGED_INSTANCE_NAMES=([0]="MongoDB") -declare -a TAGGED_INSTANCE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb") +declare -a TAGGED_INSTANCE_NAMES=() +declare -a TAGGED_INSTANCE_ARNS=() declare -a TAGGED_CERTIFICATE_NAMES=([0]="[Unnamed]") declare -a TAGGED_CERTIFICATE_ARNS=([0]="arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d") declare -a TAGGED_LOADBALANCER_NAMES=([0]="[Unnamed]") diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 989f78117d2..9f317aaef15 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -121,7 +121,7 @@ function shared_instance_execute() { local target_group_arn=$(create_target_group "S-shared-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "$server_port" "5" "4" "2" "2" - register_targets $target_group_arn $(get_instance_id_of_resource_arn $super_instance) + register_targets $target_group_arn $(get_instance_id $super_instance) local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) From 1c46dc0b441a44033418f96388ce87d7142f52a6 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 10 Feb 2018 06:25:58 +0100 Subject: [PATCH 119/214] Config files no longer needed --- .../aws-automation/lib/config_eu_west_1.sh | 23 ------------------- .../aws-automation/lib/config_eu_west_2.sh | 19 --------------- .../aws-automation/lib/functions_io.sh | 2 ++ 3 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 configuration/aws-automation/lib/config_eu_west_1.sh delete mode 100644 configuration/aws-automation/lib/config_eu_west_2.sh diff --git a/configuration/aws-automation/lib/config_eu_west_1.sh b/configuration/aws-automation/lib/config_eu_west_1.sh deleted file mode 100644 index 4e7be0a1b68..00000000000 --- a/configuration/aws-automation/lib/config_eu_west_1.sh +++ /dev/null @@ -1,23 +0,0 @@ -# Variables for region "eu-west-1" (Ireland) - -# HTTPS Listener of Application Load Balancer Sailing-eu-west-1 -# listener_arn='arn:aws:elasticloadbalancing:eu-west-1:017363970217:listener/app/Sailing-eu-west-1/32b89dbfe7f75097/f9212223209ac042' - -# Sailing Analytics App -# instance_security_group_id=sg-eaf31e85 - -# SAP Sailing Analytics App HVM 1.61 -# image_id='ami-7719840e' - -# certificate_arn='arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564' - - -default_key_name='Leon' -default_key_file='/cygdrive/c/Users/d069485/.ssh/Leon.pem' -mongodb_host='dbserver.internal.sapsailing.com' -mongodb_port='10202' -alb_domain='sapsailing.com' - -#SL Multi-Instance Sailing Server -# default_super_instance_dns_name='ec2-34-250-136-229.eu-west-1.compute.amazonaws.com' -default_ssh_user='root' diff --git a/configuration/aws-automation/lib/config_eu_west_2.sh b/configuration/aws-automation/lib/config_eu_west_2.sh deleted file mode 100644 index e2acb1ad257..00000000000 --- a/configuration/aws-automation/lib/config_eu_west_2.sh +++ /dev/null @@ -1,19 +0,0 @@ -# Variables for region "eu-west-2" (London) - -default_key_name='leonradeck-keypair' -default_key_file='/cygdrive/c/Users/d069485/.ssh/leonradeck-keypair.pem' - -# HTTPS Listener of Application Load Balancer DummyALB -# listener_arn='arn:aws:elasticloadbalancing:eu-west-2:017363970217:listener/app/DummyALB/da70f61a914fc50a/643943f49bc2a5a8' - -# Sailing Analytics App -# instance_security_group_id=sg-871732ee - -# SAP Sailing Analytics App HVM 1.56 -#image_id=ami-39f3e25d - -mongodb_host=35.176.143.232 -mongodb_port=27017 - -#default_super_instance_dns_name='' -default_ssh_user="root" diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 0a5a196c321..e4fcfb911de 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -157,6 +157,8 @@ function update_configuration(){ init_config_variable default_key_name init_config_variable default_key_file + init_config_variable default_mongodb_host + init_config_variable default_mongodb_port } function init_config_variable(){ From 729ccd7e31ea9f3e1aac350a719b914a5c85a777 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 10 Feb 2018 06:34:12 +0100 Subject: [PATCH 120/214] Removed unused variables, etc. --- .../aws-automation/lib/functions_ec2.sh | 2 +- .../aws-automation/lib/functions_elb.sh | 2 +- .../aws-automation/lib/functions_io.sh | 7 ++++ configuration/aws-automation/lib/variables.sh | 42 ++++--------------- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 2887ede1200..6e71a6dc1be 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -99,7 +99,7 @@ function wait_for_ssh_connection(){ } function run_instance(){ - aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count $instance_count --instance-type $instance_type --key-name $key_name \ + aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count 1 --instance-type $instance_type --key-name $key_name \ --security-group-ids $(get_resource_id $instance_security_group_id) --user-data "$1" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" || { safeExit; } } diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 0d8d9b5a16e..3c7e0da8832 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -9,7 +9,7 @@ function create_target_group(){ local_echo "Creating target group $1..." local target_group_name=$(only_letters_numbers_dash $1) local vpc_id=$(get_default_vpc_id) - aws_wrapper elbv2 create-target-group --name $target_group_name --protocol $target_group_protocol \ + aws_wrapper elbv2 create-target-group --name $target_group_name --protocol "$target_group_protocol" \ --port $target_group_port --vpc-id $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn' } diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index e4fcfb911de..7604b55f3e1 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -159,6 +159,13 @@ function update_configuration(){ init_config_variable default_key_file init_config_variable default_mongodb_host init_config_variable default_mongodb_port + init_config_variable default_region + init_config_variable default_instance_type + init_config_variable default_server_startup_notify + init_config_variable default_new_admin_password + init_config_variable default_user_username + init_config_variable default_user_password + } function init_config_variable(){ diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 1c00d5049e2..5cf6e6a3d21 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -1,47 +1,23 @@ #!/usr/bin/env bash -default_region=eu-west-2 -default_instance_type=t2.medium -default_server_startup_notify=leon.radeck@sap.com -default_new_admin_password=admin -default_user_username=newuser -default_user_password=newpassword latest_release=$(get_latest_release) - -# Other Variables (DONT EDIT) - autoDiscoverResourceTagName='AutoDiscover' -scriptName=`basename $0` #Set Script Name variable -scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName -declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 -datestamp=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 -hourstamp=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM -timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 -today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 -longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 -gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT -thisHost=$(hostname) - -hosted_zone_id=Z1R8UBAEXAMPLE target_group_protocol='HTTPS' target_group_port='443' ssh_retry_interval=2 http_retry_interval=5 -env_file=".env.txt" -change_resource_record_set_file=".change-resource-record-set.json" -tag_specifications="\'ResourceType=instance,Tags=[{Key=Name,Value=%s}]\'" -admin_username=admin -admin_password=admin -instance_count=1 default_instance_name="$script_start_time" default_instance_short_name="$timestamp" -sailing_0='/home/sailing/servers/server/logs/sailing0.log.0' -sailing_err='/var/log/sailing.err' -sailing_out='/var/log/sailing.out' events_conf='/etc/httpd/conf.d/001-events.conf' default_server_port='8888' default_telnet_port='14900' default_expedition_port='2000' -default_contact_person='Unknown' -default_contact_email='Unkown' -resources_file=./lib/resources-$region.sh + +scriptName=`basename $0` #Set Script Name variable +scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName +declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 +declare -r timestamp=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 +today=$(LC_ALL=C date +"%m-%d-%Y") # Returns: 06-14-2015 +longdate=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 +gmtdate=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT +thisHost=$(hostname) From b60777fff8661e71f93c2efabb0683ce499dbfb5 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sat, 10 Feb 2018 06:50:44 +0100 Subject: [PATCH 121/214] Config, removed tmux, removed util functions --- configuration/aws-automation/aws-setup.sh | 6 + .../aws-automation/lib/functions_io.sh | 28 +- .../aws-automation/lib/functions_tmux.sh | 108 ------ .../aws-automation/lib/util_functions.sh | 332 ------------------ 4 files changed, 13 insertions(+), 461 deletions(-) delete mode 100644 configuration/aws-automation/lib/functions_tmux.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 318455b99e2..a91eb5717ad 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -59,14 +59,20 @@ logFile="$HOME/Library/Logs/${scriptBasename}.log" function mainScript() { echo -n + require_region local resource_file=./lib/resources-$region.sh +local config_file=~/.aws-automation/config-$region if is_exists ${resource_file}; then source "${resource_file}" fi +if is_exists ${config_file}; then + source "${config_file}" +fi init_resources + safeExit if $instance; then instance_start diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 7604b55f3e1..d1406ce3c12 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -148,33 +148,19 @@ function init_resources(){ update_resources $resources_file fi - update_configuration + if ! is_exists ~/.aws-automation/config-$region; then + create_configuration + fi } -function update_configuration(){ +function create_configuration(){ mkdir -p ~/.aws-automation touch ~/.aws-automation/config-$region - init_config_variable default_key_name - init_config_variable default_key_file - init_config_variable default_mongodb_host - init_config_variable default_mongodb_port - init_config_variable default_region - init_config_variable default_instance_type - init_config_variable default_server_startup_notify - init_config_variable default_new_admin_password - init_config_variable default_user_username - init_config_variable default_user_password + config=$(build_configuration "default_key_name=" "default_key_file=" "default_mongodb_host=" "default_mongodb_port=" "default_region="\ + "default_instance_type=" "default_server_startup_notify=" "default_new_admin_password=" "default_user_username=" "default_user_password=") -} - -function init_config_variable(){ - local tmp=$(get_config_variable "$1") - if [ -z $tmp ]; then - set_config_variable "$1" "" - else - read -r "$1" <<< $tmp - fi + echo $config > ~/.aws-automation/config-$region } function set_config_variable(){ diff --git a/configuration/aws-automation/lib/functions_tmux.sh b/configuration/aws-automation/lib/functions_tmux.sh deleted file mode 100644 index 4a332509f89..00000000000 --- a/configuration/aws-automation/lib/functions_tmux.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -# ----------------------------------------------------------- -# Construct tmux user interface -# ----------------------------------------------------------- -function construct_ui() { - # enable scrolling, clicking on panes etc. - tmux set -g mouse on - - # enable named border at the top of panes - tmux set -g pane-border-status top - tmux set -g pane-border-format " [#{pane_index}] - #T " - - # close all panes except one - reset_panes - sleep 1 - - # construct pane layout - tmux split-window -h -p 50 - tmux select-pane -t 0 - tmux split-window -v -p 50 - tmux select-pane -t 2 - tmux split-window -v -p 50 - tmux select-pane -t 0 -} - -# ----------------------------------------------------------- -# Opens ssh connections on all three panes -# @param $1 ssh user -# @param $2 dns name of instance -# ----------------------------------------------------------- -function open_connections() { - - # Can't use ssh wrapper at this point because function cannot be export within tmux environment. - if [ -z $key_file ]; then - command="ssh -o StrictHostKeyChecking=no $1@$2" - else - command="ssh -o StrictHostKeyChecking=no -i $key_file $1@$2" - fi - - # Send $command to all three panes and execute - tmux send-keys -t 1 $command C-m - tmux send-keys -t 2 $command C-m - tmux send-keys -t 3 $command C-m -} - -# ----------------------------------------------------------- -# Close all panes except first -# ----------------------------------------------------------- -function reset_panes(){ - if more_panes_are_open; then - tmux kill-pane -a -t 0 - fi -} - -function get_number_of_panes(){ - tmux display-message -p '#{window_panes}' -} - -function more_panes_are_open(){ - if inside_tmux_session; then - if [ "$(get_number_of_panes)" -gt 1 ]; then - return 0; - fi - fi - return 1; -} - -function check_if_tmux_is_used(){ - check_dependencies - check_environment -} - -function check_dependencies() { - if ! is_tmux_available; then - echo "The package \"tmux\" is required to run the script with this option" - safeExit - fi -} - -function check_environment() { - if ! inside_tmux_session; then - echo "Please run this script inside a tmux session. To do so enter \"tmux\" into the console and start the script from there." - safeExit - fi -} - -function is_tmux_available(){ - type tmux >/dev/null 2>/dev/null -} - -function inside_tmux_session(){ - [ "$TERM" = "screen" ] && [ -n "$TMUX" ] -} - -# ----------------------------------------------------------- -# Prompts user if open panes should be closed -# ----------------------------------------------------------- -function confirm_reset_panes(){ - if more_panes_are_open; then - seek_confirmation "Do you want to close all open panes?" - if is_confirmed; then - reset_panes - else - safeExit - fi - fi -} diff --git a/configuration/aws-automation/lib/util_functions.sh b/configuration/aws-automation/lib/util_functions.sh index 300cff00ad6..d6b6789e134 100644 --- a/configuration/aws-automation/lib/util_functions.sh +++ b/configuration/aws-automation/lib/util_functions.sh @@ -265,41 +265,6 @@ function is_not_empty() { return 1 } -# Test whether a command exists -# ------------------------------------------------------ -# Usage: -# if type_exists 'git'; then -# some action -# else -# some other action -# fi -# ------------------------------------------------------ - -function type_exists() { - if [ "$(type -P "$1")" ]; then - return 0 - fi - return 1 -} - -function type_not_exists() { - if [ ! "$(type -P "$1")" ]; then - return 0 - fi - return 1 -} - -# Test which OS the user runs -# $1 = OS to test -# Usage: if is_os 'darwin'; then - -function is_os() { - if [[ "${OSTYPE}" == $1* ]]; then - return 0 - fi - return 1 -} - # SEEKING CONFIRMATION # ------------------------------------------------------ @@ -395,16 +360,6 @@ function pauseScript() { fi } -function in_array() { - # Determine if a value is in an array. - # Usage: if in_array "VALUE" "${ARRAY[@]}"; then ... - local value="$1"; shift - for arrayItem in "$@"; do - [[ "${arrayItem}" == "${value}" ]] && return 0 - done - return 1 -} - # Text Transformations # ----------------------------------- # Transform text using these functions. @@ -418,13 +373,6 @@ lower() { tr '[:upper:]' '[:lower:]' } -upper() { - # Convert stdin to uppercase. - # usage: text=$(upper <<<"$1") - # echo "MAKETHISUPPERCASE" | upper - tr '[:lower:]' '[:upper:]' -} - function lower_trim(){ echo $1 | lower | trim } @@ -434,242 +382,10 @@ trim() { tr -d '[:space:]' } -ltrim() { - # Removes all leading whitespace (from the left). - local char=${1:-[:space:]} - sed "s%^[${char//%/\\%}]*%%" -} - -rtrim() { - # Removes all trailing whitespace (from the right). - local char=${1:-[:space:]} - sed "s%[${char//%/\\%}]*$%%" -} - -lttrim() { - # Removes all leading/trailing whitespace - # Usage examples: - # echo " foo bar baz " | trim #==> "foo bar baz" - ltrim "$1" | rtrim "$1" -} - only_letters_and_numbers(){ tr -d -c '[:alnum:]' } -squeeze() { - # Removes leading/trailing whitespace and condenses all other consecutive - # whitespace into a single space. - # - # Usage examples: - # echo " foo bar baz " | squeeze #==> "foo bar baz" - - local char=${1:-[[:space:]]} - sed "s%\(${char//%/\\%}\)\+%\1%g" | trim "$char" -} - -squeeze_lines() { - # {{{ - # - # Removes all leading/trailing blank lines and condenses all other - # consecutive blank lines into a single blank line. - # - # }}} - - sed '/^[[:space:]]\+$/s/.*//g' | cat -s | trim_lines -} - -progressBar() { - # progressBar - # ----------------------------------- - # Prints a progress bar within a for/while loop. - # To use this function you must pass the total number of - # times the loop will run to the function. - # - # usage: - # for number in $(seq 0 100); do - # sleep 1 - # progressBar 100 - # done - # ----------------------------------- - if [[ "${quiet}" = "true" ]] || [ "${quiet}" == "1" ]; then - return - fi - - local width - width=30 - bar_char="#" - - # Don't run this function when scripts are running in verbose mode - if ${verbose}; then return; fi - - # Reset the count - if [ -z "${progressBarProgress}" ]; then - progressBarProgress=0 - fi - - # Do nothing if the output is not a terminal - if [ ! -t 1 ]; then - echo "Output is not a terminal" 1>&2 - return - fi - # Hide the cursor - tput civis - trap 'tput cnorm; exit 1' SIGINT - - if [ ! "${progressBarProgress}" -eq $(( $1 - 1 )) ]; then - # Compute the percentage. - perc=$(( progressBarProgress * 100 / $1 )) - # Compute the number of blocks to represent the percentage. - num=$(( progressBarProgress * width / $1 )) - # Create the progress bar string. - bar= - if [ ${num} -gt 0 ]; then - bar=$(printf "%0.s${bar_char}" $(seq 1 ${num})) - fi - # Print the progress bar. - progressBarLine=$(printf "%s [%-${width}s] (%d%%)" "Running Process" "${bar}" "${perc}") - echo -en "${progressBarLine}\r" - progressBarProgress=$(( progressBarProgress + 1 )) - else - # Clear the progress bar when complete - echo -ne "${width}%\033[0K\r" - unset progressBarProgress - fi - - tput cnorm -} - -htmlDecode() { - # Decode HTML characters with sed - # Usage: htmlDecode - echo "${1}" | sed -f "${SOURCEPATH}/htmlDecode.sed" -} - -htmlEncode() { - # Encode HTML characters with sed - # Usage: htmlEncode - echo "${1}" | sed -f "${SOURCEPATH}/htmlEncode.sed" -} - -urlencode() { - # URL encoding/decoding from: https://gist.github.com/cdown/1163649 - # Usage: urlencode - - local length="${#1}" - for (( i = 0; i < length; i++ )); do - local c="${1:i:1}" - case $c in - [a-zA-Z0-9.~_-]) printf "%s" "$c" ;; - *) printf '%%%02X' "'$c" - esac - done -} - -urldecode() { - # Usage: urldecode - - local url_encoded="${1//+/ }" - printf '%b' "${url_encoded//%/\x}" -} - -parse_yaml() { - # Function to parse YAML files and add values to variables. Send it to a temp file and source it - # https://gist.github.com/DinoChiesa/3e3c3866b51290f31243 which is derived from - # https://gist.github.com/epiloque/8cf512c6d64641bde388 - # - # Usage: - # $ parse_yaml sample.yml > /some/tempfile - # - # parse_yaml accepts a prefix argument so that imported settings all have a common prefix - # (which will reduce the risk of name-space collisions). - # - # $ parse_yaml sample.yml "CONF_" - - local prefix=$2 - local s - local w - local fs - s='[[:space:]]*' - w='[a-zA-Z0-9_]*' - fs="$(echo @|tr @ '\034')" - sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ - -e "s|^\($s\)\($w\)$s[:-]$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" | - awk -F"$fs" '{ - indent = length($1)/2; - if (length($2) == 0) { conj[indent]="+";} else {conj[indent]="";} - vname[indent] = $2; - for (i in vname) {if (i > indent) {delete vname[i]}} - if (length($3) > 0) { - vn=""; for (i=0; i> "${csvFile}" - IFS=$saveIFS - -} - -function json2yaml() { - # convert json files to yaml using python and PyYAML - python -c 'import sys, yaml, json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False)' < "$1" -} - -function yaml2json() { - # convert yaml files to json using python and PyYAML - python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=4)' < "$1" -} From 79a96e98f376381683842ce213601f23bcef087d Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 11 Feb 2018 11:19:20 +0100 Subject: [PATCH 122/214] A lot new --- configuration/aws-automation/aws-setup.sh | 23 ++- .../aws-automation/lib/functions_app.sh | 41 +++++- .../aws-automation/lib/functions_ec2.sh | 25 ++-- .../aws-automation/lib/functions_elb.sh | 9 +- .../aws-automation/lib/functions_io.sh | 136 ++++++++++++------ .../aws-automation/lib/functions_wrapper.sh | 17 ++- .../aws-automation/lib/resources-eu-west-2.sh | 6 +- .../lib/scenario_associate_alb.sh | 6 +- .../aws-automation/lib/scenario_instance.sh | 9 +- .../lib/scenario_master_instance.sh | 9 +- .../lib/scenario_shared_instance.sh | 62 ++++---- 11 files changed, 236 insertions(+), 107 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index a91eb5717ad..5ca926ffd96 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -60,20 +60,27 @@ logFile="$HOME/Library/Logs/${scriptBasename}.log" function mainScript() { echo -n -require_region +local config_file=~/.aws-automation/config -local resource_file=./lib/resources-$region.sh -local config_file=~/.aws-automation/config-$region -if is_exists ${resource_file}; then - source "${resource_file}" -fi if is_exists ${config_file}; then source "${config_file}" fi +require_region + +local resource_file=./lib/resources-$region.sh +local region_config_file=~/.aws-automation/config-$region + +if is_exists ${resource_file}; then + source "${resource_file}" +fi + +if is_exists ${region_config_file}; then + source "${region_config_file}" +fi + init_resources -safeExit if $instance; then instance_start if $associate_alb; then @@ -113,6 +120,7 @@ usage() { -a, --new-admin-password New password for the admin user -p, --public-dns-name Dns name of instance (e.g. \"ec2-35-176...amazonaws.com\") -z, --super-instance Dns name of superior instance (e.g. base instance for sub instances) + -m, --event-name Name of event -b, --build Build version to use (leave empty for latest) -w, --description Description of sub instance -c, --contact-person Contact person @@ -216,6 +224,7 @@ while [[ $1 = -?* ]]; do -a|--new-admin-password) shift; new_admin_password_param=${1} ;; -p|--public-dns-name) shift; public_dns_name_param=${1} ;; -b|--super-instance) shift; super_instance_param=${1} ;; + -m|--event-name) shift; event_name_param=${1} ;; -z|--build) shift; build_version_param=${1} ;; -w|--description) shift; description_param=${1} ;; -c|--contact-person) shift; contact_person_param=${1} ;; diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 0d353305317..abcc7cf8266 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -76,6 +76,38 @@ function create_new_user(){ curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/security/api/restsecurity/create_user?username=$4&password=$5" } +# ----------------------------------------------------------- +# Creates event, changes admin password, creates user +# @param $1 public dns name +# @param $2 port of application instance +# @param $3 event name +# @param $4 new admin password +# @param $5 user username +# @param $6 user password +# @return event id if event was created +# ----------------------------------------------------------- +function configure_application(){ + if [ -z "$3" ] && [ -z "$4" ] && [ -z "$5" ] && [ -z "$6" ]; then + return 0 + fi + + access_token=$(get_access_token "admin" "admin" $1 $2) + + if [ ! -z "$3" ]; then + event_id=$(create_event $access_token $1 $2 $3) + fi + + if [ ! -z "$4" ]; then + response=$(change_admin_password $access_token $1 $2 "admin" $4) + fi + + if [ ! -z "$5" ] && [ ! -z "$6" ]; then + user=$(create_new_user $access_token $1 $2 $5 $6) + fi + + echo ${$event_id:-""} +} + # ----------------------------------------------------------- # Patch 001-events.conf with ssl macros # @param $1 domain @@ -84,10 +116,15 @@ function create_new_user(){ # @param $4 public dns name # @param $5 server port # ----------------------------------------------------------- -function append_event_ssl_macro_to_001_events_conf(){ +function append_macro_to_001_events_conf(){ wait_for_001_events_patch $3 $4 - local patched_content="Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5" + if [ -z "$2" ]; then + patched_content="Use Home-SSL $1 127.0.0.1 $5" + else + patched_content="Use Event-SSL $1 \\\"$2\\\" 127.0.0.1 $5" + fi + local_echo "Appending´\"$patched_content\" to $events_conf..." ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" } diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 6e71a6dc1be..36a6cd97466 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -17,6 +17,15 @@ function get_auto_discover_resources(){ aws_wrapper resourcegroupstaggingapi get-resources --tag-filters "Key=$autoDiscoverResourceTagName,Values=true,True,1,Yes,yes" } +# ----------------------------------------------------------- +# @param1 instance id +# @param2 tag key +# @return tag value +# ----------------------------------------------------------- +function get_tag_value_for_key(){ + aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[*].Tags[?Key=='$2'].Value" --instance-ids $1 --output text +} + # ----------------------------------------------------------- # Query for default vpc id # @return default vpc id @@ -45,7 +54,7 @@ function get_resource_id(){ } function is_resource_arn(){ - [[ $a == "arn:*" ]] + [[ $1 == arn:* ]] } function get_public_ip(){ @@ -99,9 +108,9 @@ function wait_for_ssh_connection(){ } function run_instance(){ - aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count 1 --instance-type $instance_type --key-name $key_name \ + exit_on_fail aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count 1 --instance-type $instance_type --key-name $key_name \ --security-group-ids $(get_resource_id $instance_security_group_id) --user-data "$1" \ - --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" || { safeExit; } + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" } @@ -111,7 +120,7 @@ function run_instance(){ # @return user data of instance # ----------------------------------------------------------- function get_user_data_from_instance() { - aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize | base64 --decode + aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize } # ----------------------------------------------------------- @@ -123,13 +132,9 @@ function create_instance(){ json_instance=$(run_instance "$1") instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') wait_instance_exists $instance_id - description=$(aws --region $region ec2 describe-instances --instance-ids $instance_id \ + aws_wrapper --region $region ec2 describe-instances --instance-ids $instance_id \ --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ - --output table) - - if command_was_successful $?; then - success $description - fi + --output table echo $instance_id } diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 3c7e0da8832..cfb6abb6ab6 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -69,7 +69,14 @@ function create_rule(){ local_echo "Creating rule for listener..." local subdomain=$(alphanumeric "$2") local priority=$(($(get_rule_with_highest_priority $1) + 1)) - local domain="$subdomain.sapsailing.com" + + # For testing purpose + if [ $region == "eu-west-2" ]; then + local domain="$subdomain.dummy2.sapsailing.com" + else + local domain="$subdomain.sapsailing.com" + fi + rule=$(aws_wrapper elbv2 create-rule --listener-arn $1 --priority $priority \ --conditions Field=host-header,Values=$domain --actions Type=forward,TargetGroupArn=$3) echo $domain diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index d1406ce3c12..b4dff5d3729 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -5,15 +5,16 @@ # @param $1 prompt message # @param $2 default value # @param $3 variable that is receiving the value -# @param $4 array of resource names -# @param $5 array of resource arns +# @param $4 hide text +# @param $5 array of resource names +# @param $6 array of resource arns # ----------------------------------------------------------- function require_input(){ declare -a _optionNames=() declare -a _optionValues=() - if [ ! -z "$4" ] && [ ! -z "$5" ]; then - _optionNames=("${!4}") - _optionValues=("${!5}") + if [ ! -z "$5" ] && [ ! -z "$6" ]; then + _optionNames=("${!5}") + _optionValues=("${!6}") fi if [ ${#_optionValues[@]} -gt 1 ]; then @@ -31,7 +32,11 @@ function require_input(){ elif [ ${#_optionValues[@]} -eq 1 ]; then read -r $3 <<< ${_optionValues[0]} else - read -e -p "$1" -i "$2" $3 + if [ "$4" == "true" ]; then + read -e -s -p "$1" -i "$2" $3 + else + read -e -p "$1" -i "$2" $3 + fi fi } @@ -45,31 +50,46 @@ function require_input(){ # @param $2 variable # @param $3 default value # @param $4 prompt message -# @param $5 array of resource names -# @param $6 array of resource arns +# @param $5 true if optional +# @param $6 true if text input be hidden +# @param $7 array of resource names +# @param $8 array of resource arns # ----------------------------------------------------------- function require_variable(){ declare -a optionNames=() declare -a optionValues=() - if [ ! -z "$5" ] && [ ! -z "$6" ]; then - optionNames=("${!5}") - optionValues=("${!6}") + if [ ! -z "$7" ] && [ ! -z "$8" ]; then + optionNames=("${!7}") + optionValues=("${!8}") fi # if parameter is empty if [ -z "$1" ]; then - # if required variable is empty + # if required variable value is empty if [ -z "${!2}" ]; then # if force is enabled if [ "$force" = true ]; then - # read default value into required variable - read -r "$2" <<< "$3" + # if variable is optional + if [ "$5" == "true" ]; then + # variable should be empty + read -r "$2" <<< "" + return 0 + else + # read default value into required variable + read -r "$2" <<< "$3" + fi + fi + # if force not enabled + # if variable value is optional, then value of "" is possible + if [ "$5" == "true" ]; then + require_input "$4" "$3" $2 $6 optionNames[@] optionValues[@] + else + # if variable value is not optional, repeat until value is set for variable + while [[ -z "${!2}" ]] + do + require_input "$4" "$3" $2 $6 optionNames[@] optionValues[@] + done fi - # if force not enabled, ask user for input and repeat until value is set for variable - while [[ -z "${!2}" ]] - do - require_input "$4" "$3" $2 optionNames[@] optionValues[@] - done fi else # if parameter is not empty, read its value into required variable @@ -142,25 +162,49 @@ function update_resources(){ } function init_resources(){ + + disable_aws_success_output RESOURCES=$(get_auto_discover_resources) + enable_aws_success_output if [ "$RESOURCES" != "$OLD_RESOURCES" ]; then update_resources $resources_file fi if ! is_exists ~/.aws-automation/config-$region; then + create_region_configuration + fi + + if ! is_exists ~/.aws-automation/config; then create_configuration fi } -function create_configuration(){ +function create_region_configuration(){ mkdir -p ~/.aws-automation touch ~/.aws-automation/config-$region - config=$(build_configuration "default_key_name=" "default_key_file=" "default_mongodb_host=" "default_mongodb_port=" "default_region="\ - "default_instance_type=" "default_server_startup_notify=" "default_new_admin_password=" "default_user_username=" "default_user_password=") + local config="" + config+="default_instance_type=\n" + config+="default_ssh_user\n" + config+="default_key_name=\n" + config+="default_key_file=\n" + config+="default_mongodb_host=\n" + config+="default_mongodb_port=\n" - echo $config > ~/.aws-automation/config-$region + echo -e $config > ~/.aws-automation/config-$region +} + +function create_configuration(){ + mkdir -p ~/.aws-automation + touch ~/.aws-automation/config + + local config="" + config+="default_region=\n" + config+="default_server_startup_notify=\n" + config+="default_build_complete_notify=\n" + + echo -e $config > ~/.aws-automation/config } function set_config_variable(){ @@ -172,84 +216,92 @@ function get_config_variable(){ } function require_image_id(){ - require_variable "" image_id "" "$image_id_ask_message" TAGGED_IMAGE_NAMES[@] TAGGED_IMAGE_ARNS[@] + require_variable "" image_id "" "$image_id_ask_message" "false" "false" TAGGED_IMAGE_NAMES[@] TAGGED_IMAGE_ARNS[@] "" +} + +function require_event_name(){ + require_variable "$event_name_param" event_name "" "$event_name_ask_message" "true" "false" } function require_instance_security_group_id(){ - require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" TAGGED_SECURITY_GROUP_NAMES[@] TAGGED_SECURITY_GROUP_ARNS[@] + require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" TAGGED_SECURITY_GROUP_NAMES[@] TAGGED_SECURITY_GROUP_ARNS[@] } function require_load_balancer(){ - require_variable "" load_balancer "" "$load_balancer_ask_message" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] + require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] } function require_region(){ - require_variable "$region_param" region "$default_region" "$region_ask_message" + require_variable "$region_param" region "$default_region" "$region_ask_message" "false" "false" } function require_instance_type(){ - require_variable "$instance_type_param" instance_type "$default_instance_type" "$instance_type_ask_message" + require_variable "$instance_type_param" instance_type "$default_instance_type" "$instance_type_ask_message" "false" "false" } function require_instance_name(){ - require_variable "$instance_name_param" instance_name "$default_instance_name" "$instance_name_ask_message" + require_variable "$instance_name_param" instance_name "$default_instance_name" "$instance_name_ask_message" "false" "false" } function require_instance_short_name(){ - require_variable "$instance_short_name_param" instance_short_name "$default_instance_short_name" "$instance_short_name_ask_message" + require_variable "$instance_short_name_param" instance_short_name "$default_instance_short_name" "$instance_short_name_ask_message" "false" "false" } function require_key_name(){ - require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" + require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "false" "false" } function require_key_file(){ - require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" + require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "false" "false" } function require_new_admin_password(){ - require_variable "$new_admin_password_param" new_admin_password "$default_new_admin_password" "$new_admin_password_ask_message" + require_variable "$new_admin_password_param" new_admin_password "$default_new_admin_password" "$new_admin_password_ask_message" "true" "true" } function require_user_username(){ - require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" + require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" "true" "false" } function require_user_password(){ - require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" + require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" } function require_public_dns_name(){ - require_variable "$public_dns_name_param" public_dns_name "" "$public_dns_name_ask_message" + require_variable "$public_dns_name_param" public_dns_name "" "$public_dns_name_ask_message" "false" "false" } function require_ssh_user(){ - require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" + require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" "false" "false" } function require_super_instance(){ - require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] + require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] + + disable_aws_success_output if is_resource_arn $super_instance; then local instance_id=$(get_resource_id $super_instance) super_instance=$(query_public_dns_name $instance_id) fi + enable_aws_success_output } function require_description(){ - require_variable "$description_param" description "$instance_name" "$description_message" + require_variable "$description_param" description "" "$description_message" "true" "false" } function require_contact_person(){ - require_variable "$contact_person_param" contact_person "$default_contact_person" "$contact_person_message" + require_variable "$contact_person_param" contact_person "$default_contact_person" "$contact_person_message" "true" "false" } function require_contact_email(){ - require_variable "$contact_email_param" contact_email "$default_contact_email" "$contact_email_message" + require_variable "$contact_email_param" contact_email "$default_contact_email" "$contact_email_message" "true" "false" } function require_build_version(){ - require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" + require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" } +event_name_ask_message="Please enter an event name if you want to create an event, leave blank otherwise:" instance_security_group_id_ask_message="Please select the security group for the instance: " load_balancer_ask_message="Please select the load balancer: " region_ask_message="Please enter the region for the instance: " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index dc4b779dee0..f4ff00f0a3c 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash +declare -g DISPLAY_SUCCESS_FOR_THIS_COMMAND="true" # ----------------------------------------------------------- # Does the error checking for curl commands. # Captures response and http code of curl command into variable $out. @@ -35,7 +36,9 @@ function aws_wrapper(){ local out; out=$(aws --region $region "$@") if command_was_successful $?; then - success ${out:-"[ OK ]"} + if [ "$DISPLAY_SUCCESS_FOR_THIS_COMMAND" != "false" ]; then + success ${out:-"[ OK ]"} + fi echo $out | sanitize else error "[ ERROR ] $out" @@ -43,6 +46,14 @@ function aws_wrapper(){ fi } +function disable_aws_success_output(){ + DISPLAY_SUCCESS_FOR_THIS_COMMAND="false" +} + +function enable_aws_success_output(){ + DISPLAY_SUCCESS_FOR_THIS_COMMAND="true" +} + function ssh_prewrapper(){ if [ -z $key_file ]; then ssh -o StrictHostKeyChecking=no "$@" @@ -114,3 +125,7 @@ function fail_on_timeout(){ safeExit fi } + +function exit_on_fail(){ + "$@" || { safeExit; } +} diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh index ca6b3765edb..4c72b2dd901 100644 --- a/configuration/aws-automation/lib/resources-eu-west-2.sh +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -1,10 +1,10 @@ -declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"appSecurityGroup\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" +declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"appSecurityGroup\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"Multi Instance\", \"Key\": \"Name\" }, { \"Value\": \"root\", \"Key\": \"ssh_user\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="appSecurityGroup" [1]="[Unnamed]") declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee" [1]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c") declare -a TAGGED_IMAGE_NAMES=([0]="[Unnamed]") declare -a TAGGED_IMAGE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d") -declare -a TAGGED_INSTANCE_NAMES=() -declare -a TAGGED_INSTANCE_ARNS=() +declare -a TAGGED_INSTANCE_NAMES=([0]="Multi" [1]="Instance") +declare -a TAGGED_INSTANCE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c") declare -a TAGGED_CERTIFICATE_NAMES=([0]="[Unnamed]") declare -a TAGGED_CERTIFICATE_ARNS=([0]="arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d") declare -a TAGGED_LOADBALANCER_NAMES=([0]="[Unnamed]") diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index a3840b6ffb6..d744331b927 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -20,17 +20,17 @@ function associate_alb_require(){ function associate_alb_execute() { # create target group for instance - local target_group_arn=$(create_target_group "S-dedicated-$instance_short_name") + local target_group_arn=$(exit_on_fail create_target_group "S-dedicated-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "80" "5" "4" "2" "2" # add instance to target group register_targets $target_group_arn $instance_id # create rule for host-based forwarding within alb - local domain=$(create_rule $listener_arn $instance_short_name $target_group_arn) + local domain=$(exit_on_fail create_rule $listener_arn $instance_short_name $target_group_arn) header "Apache configuration" # configure apache with event macro - append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $public_dns_name "8888" + append_macro_to_001_events_conf $domain $event_id $ssh_user $public_dns_name "8888" } diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 944ed1fe7d6..a4c02708e76 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -36,7 +36,7 @@ function instance_require(){ function instance_execute() { header "Instance Initialization" - local user_data=$(build_configuration "MONGODB_HOST=$mongodb_host" "MONGODB_PORT=$mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + local user_data=$(build_configuration "MONGODB_HOST=$default_mongodb_host" "MONGODB_PORT=$default_mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") @@ -46,10 +46,7 @@ function instance_execute() { wait_for_ssh_connection $ssh_user $public_dns_name header "Event and user creation" + local port="8888" + configure_application $public_dns_name $port $event_name $new_admin_password $user_username $user_pass - port="8888" - access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) - event_id=$(create_event $access_token $public_dns_name $port $instance_name) - response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) - user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) } diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index 41e3a6b7434..81cc3c19bb0 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -42,18 +42,15 @@ function master_instance_execute() { "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-master-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - instance_id=$(create_instance "$user_data_master") - public_dns_name=$(query_public_dns_name $instance_id) + instance_id=$(exit_on_fail create_instance "$user_data_master") + public_dns_name=$(exit_on_fail query_public_dns_name $instance_id) wait_for_ssh_connection $ssh_user $public_dns_name header "Event and user creation" port="8888" - access_token=$(get_access_token $admin_username $admin_password $public_dns_name $port) - event_id=$(create_event $access_token $public_dns_name $port $instance_name) - response=$(change_admin_password $access_token $public_dns_name $port $admin_username $new_admin_password) - user=$(create_new_user $access_token $public_dns_name $port $user_username $user_password) + configure_application $public_dns_name $port $event_name $new_admin_password $user_username $user_password public_ip=$(get_public_ip $public_dns_name) diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 9f317aaef15..691971e86f0 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -16,7 +16,6 @@ function shared_instance_start(){ # ----------------------------------------------------------- function shared_instance_require(){ require_super_instance - require_ssh_user require_load_balancer require_key_file @@ -39,11 +38,21 @@ function shared_instance_check_preconditions(){ header "Checking preconditions" + local instance_id=$(get_instance_id $super_instance) + + echo "Checking if instance has tag: 'ssh_user'." + ssh_user=$(exit_on_fail get_tag_value_for_key $instance_id "ssh_user") + echo "Checking if directory $server_dir exists already..." - execute_remote "[ ! -d $server_dir ]" + exit_on_fail execute_remote "[ ! -d $server_dir ]" echo "Checking if ssh connection $ssh_user@$super_instance is working..." - execute_remote ls + exit_on_fail execute_remote ls + + echo "Checking if super instance has user data.\ + The instance should not have any user data set, otherwise the refreshInstance script will not work properly." + + exit_on_fail [ $(get_user_data_from_instance $instance_id) == "None" ] } function shared_instance_execute() { @@ -57,7 +66,7 @@ function shared_instance_execute() { # create sub instance server directory local_echo "Creating directory $server_dir ..." - execute_remote mkdir $server_dir + exit_on_fail execute_remote mkdir $server_dir local_echo "Getting next unused SERVER_PORT..." local server_port=$(find_first_unused_port "SERVER_PORT" $default_server_port) @@ -70,11 +79,11 @@ function shared_instance_execute() { # copy refreshInstance.sh from /servers/server to sub instance directory local_echo "Copying $refreshInstance_file to $server_dir..." - execute_remote cp $refreshInstance_file $server_dir + exit_on_fail execute_remote cp $refreshInstance_file $server_dir # Execute refreshInstance.sh local_echo "Executing refreshInstance.sh with build version $build_version ..." - execute_remote "set -o pipefail;export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" + exit_on_fail execute_remote "set -o pipefail;export DEPLOY_TO=$instance_short_name;cd $server_dir;./refreshInstance.sh install-release $build_version > /dev/null 2>&1;" # uncommenting lines containing pattern local_echo "Commenting in $comment_in_line_in_env_with_pattern inside $server_env_file..." @@ -84,41 +93,42 @@ function shared_instance_execute() { local_echo "Commenting out $comment_out_line_in_env_with_pattern inside $server_env_file..." execute_remote "sed -i '/$comment_out_line_in_env_with_pattern/s/^/#/g' $server_env_file" - local env_patch=$(build_configuration "# PATCH $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ - "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$mongodb_host" \ - "MONGODB_PORT=$mongodb_port" "DEPLOY_TO=$instance_short_name") - local_echo "Configuring $server_env_file..." - execute_remote "echo -e \"$env_patch\" >> $server_env_file" + environment="live-server" + env_content=$(wget -qO- http://releases.sapsailing.com/environments/$environment) + + exit_on_fail execute_remote "echo -e \"# START Environment: $environment \" >> $server_env_file" + exit_on_fail execute_remote "echo -e \"$env_content\" >> $server_env_file" + exit_on_fail execute_remote "echo -e \"# END Environment: $environment \" >> $server_env_file" + + local env_patch=$(build_configuration "# START Script $script_start_time" "SERVER_NAME=$(alphanumeric $instance_name)" "TELNET_PORT=$telnet_port" \ + "SERVER_PORT=$server_port" "EXPEDITION_PORT=$expedition_port" "MONGODB_NAME=$(alphanumeric $instance_name)" "MONGODB_HOST=$default_mongodb_host" \ + "MONGODB_PORT=$default_mongodb_port" "DEPLOY_TO=$instance_short_name" "SERVER_STARTUP_NOTIFY=${default_server_startup_notify:-" "}" \ + "BUILD_COMPLETE_NOTIFY=${default_build_complete_notifiy:-" "}" "# END Script $script_start_time") + + exit_on_fail execute_remote "echo -e \"$env_patch\" >> $server_env_file" + + local memory="10000m" + exit_on_fail execute_remote "echo -e \"MEMORY=$memory\" >> $server_env_file" local_echo "Creating README file if it does not exist already..." execute_remote touch $readme_file local_echo "Appending patch to README file..." - execute_remote "echo -e \"\n# $instance_short_name ($description, $contact_person, $contact_email)\n$env_patch\" >> $readme_file" + execute_remote "echo -e \"\n# $instance_short_name (${description:-"Unknown"}, ${contact_person:-"Unknown"},${contact_email:-"Unknown"})\n$env_patch\" >> $readme_file" # start server and redirect both stderr and stdout to /dev/null (&>/dev/null). Send command to background by &. Do this to avoid blocking. local_echo "Starting server..." - execute_remote -f "set -o pipefail;sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" + exit_on_fail execute_remote -f "set -o pipefail;sh -c \"cd $server_dir; nohup ./start > /dev/null 2>&1 &\"" header "Event and user creation" - # get access token - access_token=$(get_access_token $admin_username $admin_password $super_instance $server_port) - - # create event - event_id=$(create_event $access_token $super_instance $server_port $instance_name) - - # change admin password - change_admin_password $access_token $super_instance $server_port $admin_username $new_admin_password - - # create new user - user=$(create_new_user $access_token $super_instance $server_port $user_username $user_password) + event_id=$(configure_application $super_instance $port $event_name $new_admin_password $user_username $user_password) header "Configuring ALB" listener_arn=$(get_first_https_listener $load_balancer) - local target_group_arn=$(create_target_group "S-shared-$instance_short_name") + local target_group_arn=$(exit_on_fail create_target_group "S-shared-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "$server_port" "5" "4" "2" "2" register_targets $target_group_arn $(get_instance_id $super_instance) @@ -127,7 +137,7 @@ function shared_instance_execute() { header "Configuring Apache" - append_event_ssl_macro_to_001_events_conf $domain $event_id $ssh_user $super_instance $server_port + append_macro_to_001_events_conf "$domain" "$event_id" "$ssh_user" "$super_instance" "$server_port" local_echo "Reloading httpd..." out=$(execute_remote "/etc/init.d/httpd reload") From 16c0c6c94af34f653bb9c076ddae54e5acad873d Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 11 Feb 2018 11:38:52 +0100 Subject: [PATCH 123/214] more --- configuration/aws-automation/lib/functions_io.sh | 4 ++-- .../aws-automation/lib/resources-eu-west-1.sh | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 configuration/aws-automation/lib/resources-eu-west-1.sh diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index b4dff5d3729..14770699aed 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -247,11 +247,11 @@ function require_instance_short_name(){ } function require_key_name(){ - require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "false" "false" + require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "true" "false" } function require_key_file(){ - require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "false" "false" + require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "true" "false" } function require_new_admin_password(){ diff --git a/configuration/aws-automation/lib/resources-eu-west-1.sh b/configuration/aws-automation/lib/resources-eu-west-1.sh new file mode 100644 index 00000000000..0d489cd3af1 --- /dev/null +++ b/configuration/aws-automation/lib/resources-eu-west-1.sh @@ -0,0 +1,11 @@ +declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"sapsailing.com-2017\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-1:017363970217:instance/i-07344e3c868f46f85\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"SL Multi-Instance Sailling Server\", \"Key\": \"Name\" }, { \"Value\": \"Sailing\", \"Key\": \"function\" }, { \"Value\": \"sailing\", \"Key\": \"ssh_user\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-1:017363970217:image/ami-8ce485f5\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-1:017363970217:security-group/sg-eaf31e85\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-1:017363970217:loadbalancer/app/Sailing-eu-west-1/32b89dbfe7f75097\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" +declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="[Unnamed]") +declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-1:017363970217:security-group/sg-eaf31e85") +declare -a TAGGED_IMAGE_NAMES=([0]="[Unnamed]") +declare -a TAGGED_IMAGE_ARNS=([0]="arn:aws:ec2:eu-west-1:017363970217:image/ami-8ce485f5") +declare -a TAGGED_INSTANCE_NAMES=([0]="SL" [1]="Multi-Instance" [2]="Sailling" [3]="Server") +declare -a TAGGED_INSTANCE_ARNS=([0]="arn:aws:ec2:eu-west-1:017363970217:instance/i-07344e3c868f46f85") +declare -a TAGGED_CERTIFICATE_NAMES=([0]="sapsailing.com-2017") +declare -a TAGGED_CERTIFICATE_ARNS=([0]="arn:aws:acm:eu-west-1:017363970217:certificate/67a1515a-81be-472d-9459-85746d030564") +declare -a TAGGED_LOADBALANCER_NAMES=([0]="[Unnamed]") +declare -a TAGGED_LOADBALANCER_ARNS=([0]="arn:aws:elasticloadbalancing:eu-west-1:017363970217:loadbalancer/app/Sailing-eu-west-1/32b89dbfe7f75097") From 269e1c1b754668a80358aefe582cfc26df0da502 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 11 Feb 2018 11:59:15 +0100 Subject: [PATCH 124/214] Clearer messages --- .../aws-automation/lib/functions_io.sh | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 14770699aed..88b581a1abf 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -164,18 +164,24 @@ function update_resources(){ function init_resources(){ disable_aws_success_output + local_echo "Checking if aws resources with tag key 'AutoDiscover' and value 'true' were added or removed in region $region..." RESOURCES=$(get_auto_discover_resources) enable_aws_success_output if [ "$RESOURCES" != "$OLD_RESOURCES" ]; then + local_echo "Updating tagged aws resources..." update_resources $resources_file + else + local_echo "Stored aws resources are up to date." fi if ! is_exists ~/.aws-automation/config-$region; then + local_echo "Creating ~/.aws-automation/config-$region..." create_region_configuration fi if ! is_exists ~/.aws-automation/config; then + local_echo "Creating ~/.aws-automation/config..." create_configuration fi } @@ -186,7 +192,7 @@ function create_region_configuration(){ local config="" config+="default_instance_type=\n" - config+="default_ssh_user\n" + config+="default_ssh_user=\n" config+="default_key_name=\n" config+="default_key_file=\n" config+="default_mongodb_host=\n" @@ -263,7 +269,9 @@ function require_user_username(){ } function require_user_password(){ - require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" + if [ ! -z "$user_name" ]; then + require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" + fi } function require_public_dns_name(){ @@ -303,22 +311,22 @@ function require_build_version(){ event_name_ask_message="Please enter an event name if you want to create an event, leave blank otherwise:" instance_security_group_id_ask_message="Please select the security group for the instance: " -load_balancer_ask_message="Please select the load balancer: " -region_ask_message="Please enter the region for the instance: " -instance_type_ask_message="Please enter the instance type: " -key_name_ask_message="Please enter the name of your keypair to connect to the instance: " +load_balancer_ask_message="Please select/enter the load balancer: " +region_ask_message="Please enter the region you want to use (e.g. eu-west-2): " +instance_type_ask_message="Please enter the instance type (e.g. t2.medium): " +key_name_ask_message="Please enter the name of your keypair to connect to the instance (e.g. leon-keypair): " instance_name_ask_message="Please enter a name for the instance: (e.g \"WC Santander 2017\"): " instance_short_name_ask_message="Please enter a short name for the instance (e.g. \"wcs17\"): " -key_file_ask_message="Please enter the file path of the keypair or leave empty to use default ssh key: " -new_admin_password_ask_message="Please enter a new password for the admin user: " +key_file_ask_message="Please enter the file path of the keypair or leave empty to use ~/.ssh key: " +new_admin_password_ask_message="Please enter a new password for the admin user (leave blank to skip password change): " mongo_db_host_ask_message="Please enter the ip adress of the mongo db server: " mongo_db_port_ask_message="Please enter the port of the mongo db server: " -user_username_ask_message="Please enter the username of your new user: " -user_password_ask_message="Please enter the password of your new user: " +user_username_ask_message="Please enter the username of your new user (leave blank to skip user creation): " +user_password_ask_message="Please enter the password of your new user (leave blank to skip user creation): " public_dns_name_ask_message="Please enter the public dns name: " ssh_user_ask_message="Please enter the ssh user: " -super_instance_message="Please enter the dns name of the superior instance: " -description_message="Please enter a description for the server: " -contact_person_message="Please enter the name of a contact person: " -contact_email_message="Please enter the email of the contact person: " +super_instance_message="Please select/enter the dns name of the superior instance: " +description_message="Please enter a description for the server (leave blank to skip): " +contact_person_message="Please enter the name of a contact person (leave blank to skip): " +contact_email_message="Please enter the email of the contact person (leave blank to skip): " build_version_message="Please enter a build version to use (releases.sapsailing.com): " From 11a98c0be303819b9ede77a90f3330bf05d2a0c4 Mon Sep 17 00:00:00 2001 From: Radeck Date: Sun, 11 Feb 2018 11:38:17 +0000 Subject: [PATCH 125/214] Dont use wrapper here --- configuration/aws-automation/lib/functions_elb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index cfb6abb6ab6..9d192fd22d5 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -88,7 +88,7 @@ function create_rule(){ # @return highest priority # ----------------------------------------------------------- function get_rule_with_highest_priority(){ - local max_priority=$(aws elbv2 describe-rules --listener-arn $1 --query "Rules[*].{P:Priority}" --output text | sanitize | sort -n | tail -1) + local max_priority=$(aws --region=$region elbv2 describe-rules --listener-arn $1 --query "Rules[*].{P:Priority}" --output text | sanitize | sort -n | tail -1) if is_number $max_priority; then echo $max_priority else From 4df65dc0d0fbf6a67db1db2737e931f73e5d0576 Mon Sep 17 00:00:00 2001 From: Radeck Date: Sun, 11 Feb 2018 12:06:25 +0000 Subject: [PATCH 126/214] Execute remote root, apachectl configtest --- .../aws-automation/lib/scenario_shared_instance.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 691971e86f0..600a299df49 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -137,10 +137,13 @@ function shared_instance_execute() { header "Configuring Apache" - append_macro_to_001_events_conf "$domain" "$event_id" "$ssh_user" "$super_instance" "$server_port" + execute_remote_root "echo -e \"\n# $instance_short_name (${description:-"Unknown"}, ${contact_person:-"Unknown"},${contact_email:-"Unknown"})\" >> $events_conf" + append_macro_to_001_events_conf "$domain" "$event_id" "root" "$super_instance" "$server_port" local_echo "Reloading httpd..." - out=$(execute_remote "/etc/init.d/httpd reload") + if [ $(execute_remote_root "apachectl configtest") ]; then + out=$(execute_remote_root "/etc/init.d/httpd reload") + fi header "Conclusion" @@ -168,6 +171,10 @@ function execute_remote(){ ssh_wrapper $ssh_user@$super_instance "$@" } +function execute_remote_root(){ + ssh_wrapper $ssh_user@$super_instance "$@" +} + function get_last_used_port(){ ssh_prewrapper $ssh_user@$super_instance "set -o pipefail; for i in /home/sailing/servers/*/env.sh; do cat \$i | grep '^ *$1=' | tr -d '$1='; done | sort -n | tail -1" } From 9b1e0711e64ca031fe74c12087d69c097199b9a0 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Sun, 11 Feb 2018 13:37:14 +0100 Subject: [PATCH 127/214] Redirect apachectl configtest to /dev/null --- configuration/aws-automation/lib/scenario_shared_instance.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 600a299df49..b2ff861700a 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -141,7 +141,7 @@ function shared_instance_execute() { append_macro_to_001_events_conf "$domain" "$event_id" "root" "$super_instance" "$server_port" local_echo "Reloading httpd..." - if [ $(execute_remote_root "apachectl configtest") ]; then + if [ $(execute_remote_root "apachectl configtest >/dev/null 2>&1") ]; then out=$(execute_remote_root "/etc/init.d/httpd reload") fi @@ -172,7 +172,7 @@ function execute_remote(){ } function execute_remote_root(){ - ssh_wrapper $ssh_user@$super_instance "$@" + ssh_wrapper "root"@$super_instance "$@" } function get_last_used_port(){ From 94ba40a6e47ae6fa15fab348f2a3292907a90e96 Mon Sep 17 00:00:00 2001 From: Radeck Date: Sun, 11 Feb 2018 13:05:45 +0000 Subject: [PATCH 128/214] Changed condition --- configuration/aws-automation/lib/scenario_shared_instance.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index b2ff861700a..93b0c819238 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -141,9 +141,8 @@ function shared_instance_execute() { append_macro_to_001_events_conf "$domain" "$event_id" "root" "$super_instance" "$server_port" local_echo "Reloading httpd..." - if [ $(execute_remote_root "apachectl configtest >/dev/null 2>&1") ]; then - out=$(execute_remote_root "/etc/init.d/httpd reload") - fi + execute_remote_root "apachectl configtest >/dev/null 2>&1" + out=$(execute_remote_root "/etc/init.d/httpd reload") header "Conclusion" From b6ea8532ea9970039cc8946a96e2fe15ee32c1af Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 13 Feb 2018 09:13:43 +0100 Subject: [PATCH 129/214] Bug 4380: improved error handling --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index e2f26985030..edf8b58d157 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7183,7 +7183,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } return trackedRace.getRaceIdentifier(); } catch (Exception e) { - throw new RuntimeException("Error while slicing race"); + throw new RuntimeException("Error while slicing race", e); } } } From d8a7550c70b799c22697138b5acc259de60fe269 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 13 Feb 2018 14:51:46 +0100 Subject: [PATCH 130/214] Bug 4380: Fixed waiting for the TrackedRace to appear --- .../gwt/ui/server/SailingServiceImpl.java | 25 ++++++++++++------- .../impl/ExpeditionAllInOneImporter.java | 23 +++++++++++------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index edf8b58d157..fa29cea9744 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -319,7 +319,6 @@ import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.Maneuver; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.Track; import com.sap.sailing.domain.tracking.TrackedLeg; @@ -7161,14 +7160,22 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); try { - final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, + getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - - // This call waits until the RaceDefinition exists so that we can be sure that the TrackedRace also exists - // after the call returns - raceHandle.getRace(); - - final DynamicTrackedRace trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); + + DynamicTrackedRace trackedRace = null; + for (int i = 0; i < 100 ; i++) { + trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + break; + } else { + Thread.sleep(100); + } + } + + if (trackedRace == null) { + throw new IllegalStateException("Could not obtain sliced race after 10s"); + } for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources(WindSourceType.EXPEDITION)) { final WindTrack windTrackToCopyFrom = trackedRaceToSlice.getOrCreateWindTrack(windSourceToCopy); @@ -7183,7 +7190,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } return trackedRace.getRaceIdentifier(); } catch (Exception e) { - throw new RuntimeException("Error while slicing race", e); + throw new RuntimeException("Error while slicing race", e); } } } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 1b73e6b1b0d..353f6dd7b94 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -49,7 +49,6 @@ import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.trackimport.DoubleVectorFixImporter; import com.sap.sailing.domain.trackimport.GPSFixImporter; import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.gateway.trackfiles.impl.ImportResultDTO.ErrorImportDTO; import com.sap.sailing.server.gateway.trackfiles.impl.ImportResultDTO.TrackImportDTO; @@ -250,14 +249,22 @@ public class ExpeditionAllInOneImporter { raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); - final RaceHandle raceHandle = adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, + adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - - // This call waits until the RaceDefinition exists so that we can be sure that the TrackedRace also exists - // after the call returns - raceHandle.getRace(); - - final DynamicTrackedRace trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); + + DynamicTrackedRace trackedRace = null; + for (int i = 0; i < 100 ; i++) { + trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + break; + } else { + Thread.sleep(100); + } + } + + if (trackedRace == null) { + throw new IllegalStateException("Could not obtain imported race after 10s"); + } final WindImportResult windImportResult = new AbstractWindImporter.WindImportResult(); final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, From b4ad7e198d9ad8022211be406a7174b1755b4774 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 13 Feb 2018 15:01:29 +0100 Subject: [PATCH 131/214] Bug 4380: Implemented some fixes regarding copying RaceLog events to the sliced RaceLog --- .../LogEventTimeRangeWithFallbackFilter.java | 47 +++++++++++++++++++ .../gwt/ui/server/SailingServiceImpl.java | 43 +++++++++-------- 2 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java new file mode 100644 index 00000000000..ea91082ce14 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java @@ -0,0 +1,47 @@ +package com.sap.sailing.gwt.ui.server; + +import java.util.NavigableMap; +import java.util.TreeMap; + +import com.sap.sailing.domain.abstractlog.AbstractLogEvent; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; + +/** + * Helper class that filters given {@link AbstractLogEvent AbstractLogEvents} based on a given {@link TimeRange}. All + * {@link AbstractLogEvent AbstractLogEvents} in the given {@link TimeRange} plus the nearest ones outside of the + * {@link TimeRange} are retained. + */ +public class LogEventTimeRangeWithFallbackFilter> { + + private final NavigableMap filteredEvents; + private final TimeRange timeRangeToInclude; + + public LogEventTimeRangeWithFallbackFilter(TimeRange timeRangeToInclude) { + this.timeRangeToInclude = timeRangeToInclude; + this.filteredEvents = new TreeMap<>(); + } + + public void addEvent(E event) { + final TimePoint logicalTimePoint = event.getLogicalTimePoint(); + if (this.timeRangeToInclude.includes(logicalTimePoint)) { + filteredEvents.put(logicalTimePoint, event); + } else if (logicalTimePoint.before(this.timeRangeToInclude.from())) { + final TimePoint currentFallbackBefore = this.filteredEvents.lowerKey(this.timeRangeToInclude.from()); + if (currentFallbackBefore == null || logicalTimePoint.after(currentFallbackBefore)) { + this.filteredEvents.remove(currentFallbackBefore); + filteredEvents.put(logicalTimePoint, event); + } + } else if (logicalTimePoint.after(this.timeRangeToInclude.to())) { + final TimePoint currentFallbackAfter = this.filteredEvents.higherKey(this.timeRangeToInclude.to()); + if (currentFallbackAfter == null || logicalTimePoint.before(currentFallbackAfter)) { + this.filteredEvents.remove(currentFallbackAfter); + filteredEvents.put(logicalTimePoint, event); + } + } + } + + public Iterable getFilteredEvents() { + return this.filteredEvents.values(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index fa29cea9744..dde94e62a72 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7069,7 +7069,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final TimeRange timeRange = new TimeRangeImpl(sliceFrom, sliceTo); final StartTimeFinderResult startTimeFinderResult = new StartTimeFinder(getService(), raceLogOfRaceToSlice).analyze(); final TimePoint startTime = startTimeFinderResult.getStartTime(); - if (startTime != null && timeRange.includes(startTime)) { + final boolean hasStartTime = startTime != null && timeRange.includes(startTime); + if (hasStartTime) { // we do not support depdendent start times here raceLog.add(new RaceLogStartTimeEventImpl(startTimeFinderResult.getStartTime(), author, raceLog.getCurrentPassId(), startTimeFinderResult.getStartTime())); @@ -7083,6 +7084,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } + + // Only wind fixes in the new tracking interval as well as the best fallback fixes are added to the new RaceLog + final LogEventTimeRangeWithFallbackFilter windFixEvents = new LogEventTimeRangeWithFallbackFilter<>( + timeRange); for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { raceLogEvent.accept(new BaseRaceLogEventVisitor() { @Override @@ -7093,11 +7098,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogWindFixEvent event) { - final Wind windFix = event.getWindFix(); - if (timeRange.includes(windFix.getTimePoint())) { - raceLog.add(new RaceLogWindFixEventImpl(event.getLogicalTimePoint(), event.getAuthor(), - raceLog.getCurrentPassId(), windFix, event.isMagnetic())); - } + windFixEvents.addEvent(event); } @Override @@ -7108,16 +7109,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogCourseDesignChangedEvent event) { - if (!event.getLogicalTimePoint().after(endOfTracking)) { - raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getLogicalTimePoint(), - event.getAuthor(), raceLog.getCurrentPassId(), event.getCourseDesign(), - event.getCourseDesignerMode())); - } + raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getLogicalTimePoint(), + event.getAuthor(), raceLog.getCurrentPassId(), event.getCourseDesign(), + event.getCourseDesignerMode())); } @Override public void visit(RaceLogFlagEvent event) { - if (isLatestPassAndInSlicedTrackingInterval(event)) { + // If we do not have a start time in the new race, flag events won't be useful + if (hasStartTime && isLatestPass(event) && !event.getLogicalTimePoint().after(sliceTo)) { raceLog.add(new RaceLogFlagEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getUpperFlag(), event.getLowerFlag(), event.isDisplayed())); @@ -7126,30 +7126,29 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogCourseAreaChangedEvent event) { - if (isLatestPassAndInSlicedTrackingInterval(event)) { - raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), - raceLog.getCurrentPassId(), event.getCourseAreaId())); - } + raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getCourseAreaId())); } @Override public void visit(RaceLogStartProcedureChangedEvent event) { - if (isLatestPassAndInSlicedTrackingInterval(event)) { + if (hasStartTime && isLatestPass(event)) { raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getStartProcedureType())); } } - private boolean isLatestPassAndInSlicedTrackingInterval(RaceLogEvent event) { - if (event.getPassId() != raceLogOfRaceToSlice.getCurrentPassId()) { - return false; - } - return timeRange.includes(event.getLogicalTimePoint()); + private boolean isLatestPass(RaceLogEvent event) { + return event.getPassId() == raceLogOfRaceToSlice.getCurrentPassId(); } // TODO do we need to copy more events? }); } + windFixEvents.getFilteredEvents() + .forEach(event -> raceLog.add(new RaceLogWindFixEventImpl(event.getLogicalTimePoint(), + event.getAuthor(), raceLog.getCurrentPassId(), event.getWindFix(), event.isMagnetic()))); + final TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis // that's why we can't just call adapter.denoteRaceForRaceLogTracking From 7e838ab23d4f48bd091c8bac1f8b66c7e2ce92dc Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 13 Feb 2018 16:15:29 +0100 Subject: [PATCH 132/214] Bug 4380: Fixed NPE --- .../ui/server/LogEventTimeRangeWithFallbackFilter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java index ea91082ce14..8ba4f39fb43 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/LogEventTimeRangeWithFallbackFilter.java @@ -29,13 +29,17 @@ public class LogEventTimeRangeWithFallbackFilter> } else if (logicalTimePoint.before(this.timeRangeToInclude.from())) { final TimePoint currentFallbackBefore = this.filteredEvents.lowerKey(this.timeRangeToInclude.from()); if (currentFallbackBefore == null || logicalTimePoint.after(currentFallbackBefore)) { - this.filteredEvents.remove(currentFallbackBefore); + if (currentFallbackBefore != null) { + this.filteredEvents.remove(currentFallbackBefore); + } filteredEvents.put(logicalTimePoint, event); } } else if (logicalTimePoint.after(this.timeRangeToInclude.to())) { final TimePoint currentFallbackAfter = this.filteredEvents.higherKey(this.timeRangeToInclude.to()); if (currentFallbackAfter == null || logicalTimePoint.before(currentFallbackAfter)) { - this.filteredEvents.remove(currentFallbackAfter); + if (currentFallbackAfter != null) { + this.filteredEvents.remove(currentFallbackAfter); + } filteredEvents.put(logicalTimePoint, event); } } From 09af4f83ac9c1ad9b4509b995c2c5815db7029f4 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 13 Feb 2018 16:19:33 +0100 Subject: [PATCH 133/214] Added exit assertion --- configuration/aws-automation/lib/scenario_instance.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index a4c02708e76..38ab173e1be 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -40,8 +40,8 @@ function instance_execute() { "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - instance_id=$(create_instance "$user_data") - public_dns_name=$(query_public_dns_name $instance_id) + instance_id=$(exit_on_fail create_instance "$user_data") + public_dns_name=$(exit_on_fail query_public_dns_name $instance_id) wait_for_ssh_connection $ssh_user $public_dns_name From 99bb53e2c8ff16721ee48ade35e09eaa82b70e39 Mon Sep 17 00:00:00 2001 From: Radeck Date: Tue, 13 Feb 2018 15:39:05 +0000 Subject: [PATCH 134/214] Adjusted input display --- configuration/aws-automation/lib/functions_io.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 88b581a1abf..ef779658169 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -18,22 +18,25 @@ function require_input(){ fi if [ ${#_optionValues[@]} -gt 1 ]; then - local_echo "--- Names ---" + local_echo "$1" + local_echo "--- Select by Name ---" o=0 for name in ${optionNames[@]}; do local_echo "$((++o))) $name" done - local_echo "--- Resource IDs ---" + local_echo "--- Select by Resource ID ---" select option in ${_optionValues[@]}; do read -r $3 <<< $option - break + return 0 done elif [ ${#_optionValues[@]} -eq 1 ]; then read -r $3 <<< ${_optionValues[0]} else if [ "$4" == "true" ]; then read -e -s -p "$1" -i "$2" $3 + # print new line after read -s + echo else read -e -p "$1" -i "$2" $3 fi @@ -77,6 +80,7 @@ function require_variable(){ else # read default value into required variable read -r "$2" <<< "$3" + return 0 fi fi # if force not enabled From 204f8603b85d996d6c1517f195e83fcda86447c7 Mon Sep 17 00:00:00 2001 From: Radeck Date: Tue, 13 Feb 2018 19:59:04 +0000 Subject: [PATCH 135/214] Added validation --- configuration/aws-automation/aws-setup.sh | 5 ++- .../aws-automation/lib/functions_ec2.sh | 39 ++++++++++--------- .../aws-automation/lib/functions_io.sh | 28 +++++++------ .../aws-automation/lib/functions_wrapper.sh | 4 +- .../aws-automation/lib/scenario_instance.sh | 17 ++++---- .../lib/scenario_master_instance.sh | 2 +- .../aws-automation/lib/util_functions.sh | 16 +------- .../aws-automation/lib/validation.sh | 27 +++++++++++++ 8 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 configuration/aws-automation/lib/validation.sh diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 5ca926ffd96..b183154c3c5 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -27,7 +27,7 @@ function trapCleanup() { function safeExit() { deleteTemp trap - INT TERM EXIT - exit + exit 77 } function deleteTemp () { @@ -265,6 +265,9 @@ if ${strict}; then set -o nounset ; fi # This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example. set -o pipefail +set -E +trap '[ "$?" -ne 77 ] || exit 77' ERR + # Invoke the checkDependenices function to test for Bash packages. Uncomment if needed. # checkDependencies diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 36a6cd97466..8ba369f8418 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -5,8 +5,9 @@ # @param $1 instance id # @return public dns name # ----------------------------------------------------------- -function query_public_dns_name(){ - aws_wrapper ec2 describe-instances --instance-ids $1 --output text --query 'Reservations[*].Instances[*].PublicDnsName' +function get_public_dns_name(){ + local public_dns_name=$(aws_wrapper ec2 describe-instances --instance-ids $1 --output text --query 'Reservations[*].Instances[*].PublicDnsName') + exit_on_fail validate_hostname $public_dns_name } # ----------------------------------------------------------- @@ -41,25 +42,23 @@ function get_default_vpc_id(){ # ----------------------------------------------------------- function get_instance_id(){ local_echo "Querying for the instance id of $1..." - aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text + local instance_id=$(aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicDnsName=='$1'].InstanceId" --output text) + exit_on_fail validate_instance_id $instance_id } # ----------------------------------------------------------- # Returns the instance id part of e.g. arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb # param $1 resource arn -# @return instance id +# @return id part of arn # ----------------------------------------------------------- function get_resource_id(){ cut -d/ -f2 <<< $1 } -function is_resource_arn(){ - [[ $1 == arn:* ]] -} - function get_public_ip(){ local_echo "Querying for the public ip of $1..." - aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicIpAddress=='$1'].InstanceId" --output text + local ip_address=$(aws_wrapper ec2 describe-instances --query "Reservations[*].Instances[?PublicIpAddress=='$1'].InstanceId" --output text) + exit_on_fail validate_ipaddress $ip_address } # ----------------------------------------------------------- @@ -108,19 +107,19 @@ function wait_for_ssh_connection(){ } function run_instance(){ - exit_on_fail aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count 1 --instance-type $instance_type --key-name $key_name \ + local instance_id=$(aws_wrapper ec2 run-instances --image-id $(get_resource_id $image_id) --count 1 --instance-type $instance_type --key-name $key_name \ --security-group-ids $(get_resource_id $instance_security_group_id) --user-data "$1" \ - --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$instance_name}]" | get_attribute '.Instances[0].InstanceId') + exit_on_fail validate_instance_id $instance_id } - # ----------------------------------------------------------- # Get user data of instance with specific instance id # @param $1 instance id # @return user data of instance # ----------------------------------------------------------- function get_user_data_from_instance() { - aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize + aws_wrapper ec2 describe-instance-attribute --instance-id $1 --attribute userData --output text --query "UserData.Value" | sanitize } # ----------------------------------------------------------- @@ -129,12 +128,14 @@ function get_user_data_from_instance() { # ----------------------------------------------------------- function create_instance(){ local_echo -e "Creating instance with following specifications:\n\nRegion: $region\nName: $instance_name\nShort name: $instance_short_name\nType: $instance_type\nBuild: $build_version\n\nUser data:\n${1}\n" - json_instance=$(run_instance "$1") - instance_id=$(echo "$json_instance" | get_attribute '.Instances[0].InstanceId') + instance_id=$(run_instance "$1") wait_instance_exists $instance_id - aws_wrapper --region $region ec2 describe-instances --instance-ids $instance_id \ - --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ - --output table - + print_instance_description $instance_id echo $instance_id } + +function print_instance_description(){ + aws_wrapper ec2 describe-instances --instance-ids $1 \ + --query "Reservations[*].Instances[0].{InstanceId:InstanceId, ImageId:ImageId, Type:InstanceType, PublicDNS:PublicDnsName, KeyName:KeyName, PrivateDnsName:PrivateDnsName, PrivateIpAddress:PrivateIpAddress}"\ + --output table 1>&2 +} diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index ef779658169..e3dbbb39b61 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -35,7 +35,7 @@ function require_input(){ else if [ "$4" == "true" ]; then read -e -s -p "$1" -i "$2" $3 - # print new line after read -s + # print new line after read -s echo else read -e -p "$1" -i "$2" $3 @@ -132,6 +132,7 @@ function get_names_for_resources(){ echo ${names[@]} } + function update_resources(){ local resources_file=./lib/resources-$region.sh > $resources_file @@ -257,11 +258,15 @@ function require_instance_short_name(){ } function require_key_name(){ - require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "true" "false" + if [ -z ${key_file+x} ] || [ ! -z "$key_file" ]; then + require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "true" "false" + fi } function require_key_file(){ - require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "true" "false" + if [ -z ${key_name+x} ] || [ ! -z "$key_name" ]; then + require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "true" "false" + fi } function require_new_admin_password(){ @@ -269,11 +274,13 @@ function require_new_admin_password(){ } function require_user_username(){ - require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" "true" "false" + if [ ! -z ${user_password+x} ] || [ ! -z "$user_password" ]; then + require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" "true" "false" + fi } function require_user_password(){ - if [ ! -z "$user_name" ]; then + if [ ! -z ${user_name+x} ] || [ ! -z "$user_name" ]; then require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" fi } @@ -292,7 +299,7 @@ function require_super_instance(){ disable_aws_success_output if is_resource_arn $super_instance; then local instance_id=$(get_resource_id $super_instance) - super_instance=$(query_public_dns_name $instance_id) + super_instance=$(get_public_dns_name $instance_id) fi enable_aws_success_output } @@ -313,12 +320,12 @@ function require_build_version(){ require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" } -event_name_ask_message="Please enter an event name if you want to create an event, leave blank otherwise:" +event_name_ask_message="Please enter an event name (leave blank to skip event creation):" instance_security_group_id_ask_message="Please select the security group for the instance: " -load_balancer_ask_message="Please select/enter the load balancer: " +load_balancer_ask_message="Please select/enter the load balancer dns name: " region_ask_message="Please enter the region you want to use (e.g. eu-west-2): " instance_type_ask_message="Please enter the instance type (e.g. t2.medium): " -key_name_ask_message="Please enter the name of your keypair to connect to the instance (e.g. leon-keypair): " +key_name_ask_message="Please enter the name of your keypair to connect to the instance (e.g. leon-keypair). Leave blank to use ~/.ssh key: " instance_name_ask_message="Please enter a name for the instance: (e.g \"WC Santander 2017\"): " instance_short_name_ask_message="Please enter a short name for the instance (e.g. \"wcs17\"): " key_file_ask_message="Please enter the file path of the keypair or leave empty to use ~/.ssh key: " @@ -327,8 +334,7 @@ mongo_db_host_ask_message="Please enter the ip adress of the mongo db server: " mongo_db_port_ask_message="Please enter the port of the mongo db server: " user_username_ask_message="Please enter the username of your new user (leave blank to skip user creation): " user_password_ask_message="Please enter the password of your new user (leave blank to skip user creation): " -public_dns_name_ask_message="Please enter the public dns name: " -ssh_user_ask_message="Please enter the ssh user: " +ssh_user_ask_message="Please enter the ssh user to connect to the instance: " super_instance_message="Please select/enter the dns name of the superior instance: " description_message="Please enter a description for the server (leave blank to skip): " contact_person_message="Please enter the name of a contact person (leave blank to skip): " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index f4ff00f0a3c..4e784b63539 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -34,7 +34,7 @@ function curl_wrapper(){ # ----------------------------------------------------------- function aws_wrapper(){ local out; - out=$(aws --region $region "$@") + out=$(aws --region $region "$@" 2>&1) if command_was_successful $?; then if [ "$DISPLAY_SUCCESS_FOR_THIS_COMMAND" != "false" ]; then success ${out:-"[ OK ]"} @@ -42,7 +42,7 @@ function aws_wrapper(){ echo $out | sanitize else error "[ ERROR ] $out" - return 1 + safeExit fi } diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 38ab173e1be..abc045ba213 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -34,19 +34,22 @@ function instance_require(){ # @param $1 user data # ----------------------------------------------------------- function instance_execute() { + header "Instance Initialization" - local user_data=$(build_configuration "MONGODB_HOST=$default_mongodb_host" "MONGODB_PORT=$default_mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ - "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ - "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") - - instance_id=$(exit_on_fail create_instance "$user_data") - public_dns_name=$(exit_on_fail query_public_dns_name $instance_id) - + local user_data=$(build_user_data) + instance_id=$(create_instance "$user_data") + public_dns_name=$(get_public_dns_name $instance_id) wait_for_ssh_connection $ssh_user $public_dns_name header "Event and user creation" + local port="8888" configure_application $public_dns_name $port $event_name $new_admin_password $user_username $user_pass +} +function build_user_data(){ + build_configuration "MONGODB_HOST=$default_mongodb_host" "MONGODB_PORT=$default_mongodb_port" "MONGODB_NAME=$(alphanumeric $instance_name)" \ + "REPLICATION_CHANNEL=$(alphanumeric $instance_name)" "SERVER_NAME=$(alphanumeric $instance_name)" "USE_ENVIRONMENT=live-server" \ + "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify" } diff --git a/configuration/aws-automation/lib/scenario_master_instance.sh b/configuration/aws-automation/lib/scenario_master_instance.sh index 81cc3c19bb0..a075900f6d1 100644 --- a/configuration/aws-automation/lib/scenario_master_instance.sh +++ b/configuration/aws-automation/lib/scenario_master_instance.sh @@ -43,7 +43,7 @@ function master_instance_execute() { "INSTALL_FROM_RELEASE=$build_version" "SERVER_STARTUP_NOTIFY=$default_server_startup_notify") instance_id=$(exit_on_fail create_instance "$user_data_master") - public_dns_name=$(exit_on_fail query_public_dns_name $instance_id) + public_dns_name=$(get_public_dns_name $instance_id) wait_for_ssh_connection $ssh_user $public_dns_name diff --git a/configuration/aws-automation/lib/util_functions.sh b/configuration/aws-automation/lib/util_functions.sh index d6b6789e134..91d1cf6c944 100644 --- a/configuration/aws-automation/lib/util_functions.sh +++ b/configuration/aws-automation/lib/util_functions.sh @@ -37,6 +37,7 @@ function uncomment_line_starting_with(){ sed -i '/$1/s/^#//g' $2 } + # ----------------------------------------------------------- # Checks if variable is a number # @param $1 returnvariablevalue @@ -46,21 +47,6 @@ function is_number(){ [[ $1 =~ ^-?[0-9]+$ ]] } -# ----------------------------------------------------------- -# Finds first missing number in array of sorted numbers -# @param $1 array of numbers -# @return first missing number -# ----------------------------------------------------------- -function find_first_missing_number_in_array(){ - arr=("$@") - for (( i = 0 ; i < $((${#arr[@]})) ; i++ )); do - if [ "$((${arr[$i]}+1))" != "${arr[$(($i + 1))]}" ] ;then - missing_number="$((${arr[$i]}+1))" - break - fi - done - echo $missing_number -} # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/validation.sh b/configuration/aws-automation/lib/validation.sh new file mode 100644 index 00000000000..0ee358773e2 --- /dev/null +++ b/configuration/aws-automation/lib/validation.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +ValidHostnameRegex='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' +ValidIpAddressRegex='^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' +ValidInstanceIdRegex='i-[a-zA-Z0-9]{17}' +ValidResourceARNRegex="arn:.+" + +function validate_resource_arn(){ + [[ "$1" =~ $ValidResourceARNRegex ]] && echo "$1" || error_message "ResourceARN \"$1\" not valid." +} + +function validate_instance_id(){ + [[ "$1" =~ $ValidInstanceIdRegex ]] && echo "$1" || error_message "Instance id \"$1\" not valid." +} + +function validate_hostname(){ + [[ "$1" =~ $ValidHostnameRegex ]] && echo "$1" || error_message "Hostname \"$1\" not valid." +} + +function validate_ipaddress(){ + [[ "$1" =~ $ValidIpAddressRegex ]] && echo "$1" || error_message "Ip address \"$1\" not valid." +} + +function error_message(){ + error "[ ERROR ] $1 (${FUNCNAME[@]})" + return 1 +} From 849854aae6d12bd7c6a4cdbffd9b787e09765e5f Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Tue, 13 Feb 2018 21:03:22 +0100 Subject: [PATCH 136/214] Revert stderr redirection --- configuration/aws-automation/lib/functions_wrapper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 4e784b63539..82268619270 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -34,7 +34,7 @@ function curl_wrapper(){ # ----------------------------------------------------------- function aws_wrapper(){ local out; - out=$(aws --region $region "$@" 2>&1) + out=$(aws --region $region "$@") if command_was_successful $?; then if [ "$DISPLAY_SUCCESS_FOR_THIS_COMMAND" != "false" ]; then success ${out:-"[ OK ]"} From 8d1f36575c1af0883858f0d5392f96a7fa6f23ab Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Wed, 14 Feb 2018 02:58:08 +0100 Subject: [PATCH 137/214] Store all resources in associative array --- configuration/aws-automation/aws-setup.sh | 9 +- .../aws-automation/lib/functions_ec2.sh | 5 +- .../aws-automation/lib/functions_io.sh | 85 ++++++------------- .../aws-automation/lib/functions_wrapper.sh | 2 +- .../aws-automation/lib/resources-eu-west-2.sh | 27 +++--- .../lib/scenario_associate_alb.sh | 12 ++- .../aws-automation/lib/scenario_instance.sh | 1 + .../lib/scenario_shared_instance.sh | 10 ++- 8 files changed, 71 insertions(+), 80 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index b183154c3c5..d40fa7607da 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -81,11 +81,9 @@ fi init_resources +safeExit if $instance; then instance_start - if $associate_alb; then - associate_alb_start - fi safeExit fi @@ -99,6 +97,11 @@ if $master_instance; then safeExit fi +if $associate_alb; then + associate_alb_start + safeExit +fi + } usage() { diff --git a/configuration/aws-automation/lib/functions_ec2.sh b/configuration/aws-automation/lib/functions_ec2.sh index 8ba369f8418..6ebcea62940 100644 --- a/configuration/aws-automation/lib/functions_ec2.sh +++ b/configuration/aws-automation/lib/functions_ec2.sh @@ -10,12 +10,13 @@ function get_public_dns_name(){ exit_on_fail validate_hostname $public_dns_name } + # ----------------------------------------------------------- # Get resources that should be automatically discovered # @return array of resource ids # ----------------------------------------------------------- -function get_auto_discover_resources(){ - aws_wrapper resourcegroupstaggingapi get-resources --tag-filters "Key=$autoDiscoverResourceTagName,Values=true,True,1,Yes,yes" +function get_resources(){ + aws resourcegroupstaggingapi get-resources | jq -c ".ResourceTagMappingList[] | select(.ResourceARN)" -r | sanitize } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index e3dbbb39b61..96ee88fc266 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -109,73 +109,41 @@ function local_echo(){ echo "$@" >&2 } -function select_resource_names(){ - jq -c ".ResourceTagMappingList[] | select(.ResourceARN | contains(\"$1\")) | .Tags[] | select(.Key==\"Name\") | .Value" -r | sanitize -} - -function select_resource_arns(){ - jq -c ".ResourceTagMappingList[] | select(.ResourceARN | contains(\"$1\")) | .ResourceARN" -r | sanitize -} - -function get_names_for_resources(){ - declare -a resources=("${!1}") - declare -a names=() - - for arn in ${resources[@]}; do - name=$(echo "$RESOURCES" | select_resource_names $arn) - if [ -z "$name" ]; then - names+=("[Unnamed]") - else - names+=("$name") - fi - done - echo ${names[@]} -} - - function update_resources(){ local resources_file=./lib/resources-$region.sh > $resources_file - OLD_RESOURCES=$RESOURCES - typeset -p OLD_RESOURCES >> $resources_file + OLD_RESOURCE_JSON=$RESOURCE_JSON + typeset -p OLD_RESOURCE_JSON >> $resources_file - local filter="security-group" - TAGGED_SECURITY_GROUP_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) - IFS=' ' read -ra TAGGED_SECURITY_GROUP_NAMES <<< "$(get_names_for_resources TAGGED_SECURITY_GROUP_ARNS[@])" - typeset -p TAGGED_SECURITY_GROUP_NAMES TAGGED_SECURITY_GROUP_ARNS >> $resources_file + RESOURCES_ARRAY=($(get_resources)) - local filter="image" - TAGGED_IMAGE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) - IFS=' ' read -ra TAGGED_IMAGE_NAMES <<< "$(get_names_for_resources TAGGED_IMAGE_ARNS[@])" - typeset -p TAGGED_IMAGE_NAMES TAGGED_IMAGE_ARNS >> $resources_file + declare -A RESOURCE_MAP - local filter="instance" - TAGGED_INSTANCE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) - IFS=' ' read -ra TAGGED_INSTANCE_NAMES <<< "$(get_names_for_resources TAGGED_INSTANCE_ARNS[@])" - typeset -p TAGGED_INSTANCE_NAMES TAGGED_INSTANCE_ARNS >> $resources_file + for resource in ${RESOURCES_ARRAY[@]}; do + local arn=$(echo $resource | jq -c ".ResourceARN" -r | sanitize) + local name=$(echo $resource | jq -c ".Tags[] | select(.Key==\"Name\") | .Value" -r | sanitize) + local tagged=$(echo $resource | jq -c ".Tags[] | select(.Key==\"AutoDiscover\") | .Value" -r | sanitize) - local filter="certificate" - TAGGED_CERTIFICATE_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) - IFS=' ' read -ra TAGGED_CERTIFICATE_NAMES <<< "$(get_names_for_resources TAGGED_CERTIFICATE_ARNS[@])" - typeset -p TAGGED_CERTIFICATE_NAMES TAGGED_CERTIFICATE_ARNS >> $resources_file + if [ ! -z "$tagged" ] && [ "$tagged" == "true" ] || [ "$tagged" == "True" ]; then + tagged="true" + else + tagged="false" + fi - local filter="loadbalancer" - TAGGED_LOADBALANCER_ARNS=($(echo "$RESOURCES" | select_resource_arns $filter )) - IFS=' ' read -ra TAGGED_LOADBALANCER_NAMES <<< "$(get_names_for_resources TAGGED_LOADBALANCER_ARNS[@])" - typeset -p TAGGED_LOADBALANCER_NAMES TAGGED_LOADBALANCER_ARNS >> $resources_file + RESOURCE_MAP[$arn]="${name:-"Unnamed"}, $tagged" + done + typeset -p RESOURCE_MAP >> $resources_file } + function init_resources(){ - - disable_aws_success_output local_echo "Checking if aws resources with tag key 'AutoDiscover' and value 'true' were added or removed in region $region..." - RESOURCES=$(get_auto_discover_resources) - enable_aws_success_output + RESOURCE_JSON=$(get_resources) - if [ "$RESOURCES" != "$OLD_RESOURCES" ]; then + if [ "$RESOURCE_JSON" != "$OLD_RESOURCE_JSON" ]; then local_echo "Updating tagged aws resources..." - update_resources $resources_file + update_resources else local_echo "Stored aws resources are up to date." fi @@ -237,10 +205,15 @@ function require_event_name(){ function require_instance_security_group_id(){ require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" TAGGED_SECURITY_GROUP_NAMES[@] TAGGED_SECURITY_GROUP_ARNS[@] } + function require_load_balancer(){ require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] } +function require_instance(){ + require_variable "" instance "" "$instance_ask_message" "false" "false" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] +} + function require_region(){ require_variable "$region_param" region "$default_region" "$region_ask_message" "false" "false" } @@ -295,13 +268,6 @@ function require_ssh_user(){ function require_super_instance(){ require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] - - disable_aws_success_output - if is_resource_arn $super_instance; then - local instance_id=$(get_resource_id $super_instance) - super_instance=$(get_public_dns_name $instance_id) - fi - enable_aws_success_output } function require_description(){ @@ -320,6 +286,7 @@ function require_build_version(){ require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" } +instance_ask_message="Please enter an instance to use: " event_name_ask_message="Please enter an event name (leave blank to skip event creation):" instance_security_group_id_ask_message="Please select the security group for the instance: " load_balancer_ask_message="Please select/enter the load balancer dns name: " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 82268619270..4e784b63539 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -34,7 +34,7 @@ function curl_wrapper(){ # ----------------------------------------------------------- function aws_wrapper(){ local out; - out=$(aws --region $region "$@") + out=$(aws --region $region "$@" 2>&1) if command_was_successful $?; then if [ "$DISPLAY_SUCCESS_FOR_THIS_COMMAND" != "false" ]; then success ${out:-"[ OK ]"} diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh index 4c72b2dd901..3a8468cc3ec 100644 --- a/configuration/aws-automation/lib/resources-eu-west-2.sh +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -1,11 +1,16 @@ -declare -- OLD_RESOURCES="{ \"ResourceTagMappingList\": [ { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"appSecurityGroup\", \"Key\": \"Name\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" }, { \"Value\": \"Multi Instance\", \"Key\": \"Name\" }, { \"Value\": \"root\", \"Key\": \"ssh_user\" } ] }, { \"ResourceARN\": \"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] }, { \"ResourceARN\": \"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\", \"Tags\": [ { \"Value\": \"true\", \"Key\": \"AutoDiscover\" } ] } ] }" -declare -a TAGGED_SECURITY_GROUP_NAMES=([0]="appSecurityGroup" [1]="[Unnamed]") -declare -a TAGGED_SECURITY_GROUP_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee" [1]="arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c") -declare -a TAGGED_IMAGE_NAMES=([0]="[Unnamed]") -declare -a TAGGED_IMAGE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d") -declare -a TAGGED_INSTANCE_NAMES=([0]="Multi" [1]="Instance") -declare -a TAGGED_INSTANCE_ARNS=([0]="arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c") -declare -a TAGGED_CERTIFICATE_NAMES=([0]="[Unnamed]") -declare -a TAGGED_CERTIFICATE_ARNS=([0]="arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d") -declare -a TAGGED_LOADBALANCER_NAMES=([0]="[Unnamed]") -declare -a TAGGED_LOADBALANCER_ARNS=([0]="arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a") +declare -- OLD_RESOURCE_JSON="{\"ResourceARN\":\"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f\",\"Tags\":[{\"Value\":\"12\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb\",\"Tags\":[{\"Value\":\"MongoDB\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562\",\"Tags\":[{\"Value\":\"yxc\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"},{\"Value\":\"Multi Instance\",\"Key\":\"Name\"},{\"Value\":\"root\",\"Key\":\"ssh_user\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916\",\"Tags\":[{\"Value\":\"Database and Messaging\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"},{\"Value\":\"appSecurityGroup\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff\",\"Tags\":[{\"Value\":\"test\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]}" +declare -A RESOURCE_MAP=([arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb]="MongoDB, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680]="123, false" [arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d]="Unnamed, true" [arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee]="appSecurityGroup, true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562]="yxc, false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff]="test, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f]="12, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c]="Multi Instance, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916]="Database and Messaging, false" ) diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index d744331b927..f7122ea1e7c 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -15,7 +15,15 @@ function associate_alb_start(){ # ----------------------------------------------------------- function associate_alb_require(){ # will only be executed after instance creation - : + require_instance + require_instance_short_name + require_load_balancer + require_ssh_user + + disable_aws_success_output + instance_id=$(get_resource_id $instance) + public_dns_name=$(get_public_dns_name $instance_id) + enable_aws_success_output } function associate_alb_execute() { @@ -23,7 +31,7 @@ function associate_alb_execute() { local target_group_arn=$(exit_on_fail create_target_group "S-dedicated-$instance_short_name") set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "80" "5" "4" "2" "2" - # add instance to target group + listener_arn=$(get_first_https_listener )# add instance to target group register_targets $target_group_arn $instance_id # create rule for host-based forwarding within alb diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index abc045ba213..4139d821e7c 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -24,6 +24,7 @@ function instance_require(){ require_build_version require_key_name require_key_file + require_event_name require_new_admin_password require_user_username require_user_password diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index 93b0c819238..a0e232c7701 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -18,10 +18,11 @@ function shared_instance_require(){ require_super_instance require_load_balancer - require_key_file require_instance_name require_instance_short_name require_build_version + require_key_file + require_event_name require_new_admin_password require_user_username require_user_password @@ -29,6 +30,11 @@ function shared_instance_require(){ require_contact_person require_contact_email require_description + + disable_aws_success_output + local instance_id=$(get_resource_id $super_instance) + super_instance=$(get_public_dns_name $instance_id) + enable_aws_success_output } function shared_instance_check_preconditions(){ @@ -137,7 +143,7 @@ function shared_instance_execute() { header "Configuring Apache" - execute_remote_root "echo -e \"\n# $instance_short_name (${description:-"Unknown"}, ${contact_person:-"Unknown"},${contact_email:-"Unknown"})\" >> $events_conf" + execute_remote_root "echo -e \"\n# $instance_short_name (${description:-"Unknown"}, ${contact_person:-"Unknown"}, ${contact_email:-"Unknown"})\" >> $events_conf" append_macro_to_001_events_conf "$domain" "$event_id" "root" "$super_instance" "$server_port" local_echo "Reloading httpd..." From 6ece5c3817ca320bcb141a9b00926a21d6b28f5a Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Wed, 14 Feb 2018 05:39:16 +0100 Subject: [PATCH 138/214] Tagged resources are highlighted within menu --- configuration/aws-automation/aws-setup.sh | 2 +- .../aws-automation/lib/functions_io.sh | 144 ++++++++++++------ .../aws-automation/lib/resources-eu-west-2.sh | 2 +- 3 files changed, 103 insertions(+), 45 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index d40fa7607da..60c51646695 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -80,7 +80,7 @@ if is_exists ${region_config_file}; then fi init_resources - +require_instance_arn safeExit if $instance; then instance_start diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 96ee88fc266..af13e8b5c81 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -9,37 +9,103 @@ # @param $5 array of resource names # @param $6 array of resource arns # ----------------------------------------------------------- -function require_input(){ - declare -a _optionNames=() - declare -a _optionValues=() - if [ ! -z "$5" ] && [ ! -z "$6" ]; then - _optionNames=("${!5}") - _optionValues=("${!6}") - fi +# function require_input(){ +# declare -a _optionNames=() +# declare -a _optionValues=() +# if [ ! -z "$5" ] && [ ! -z "$6" ]; then +# _optionNames=("${!5}") +# _optionValues=("${!6}") +# fi +# +# if [ ${#_optionValues[@]} -gt 1 ]; then +# local_echo "$1" +# local_echo "--- Select by Name ---" +# o=0 +# for name in ${optionNames[@]}; do +# local_echo "$((++o))) $name" +# done +# local_echo "--- Select by Resource ID ---" +# select option in ${_optionValues[@]}; +# do +# read -r $3 <<< $option +# return 0 +# done +# elif [ ${#_optionValues[@]} -eq 1 ]; then +# read -r $3 <<< ${_optionValues[0]} +# else +# if [ "$4" == "true" ]; then +# read -e -s -p "$1" -i "$2" $3 +# # print new line after read -s +# echo +# else +# read -e -p "$1" -i "$2" $3 +# fi +# fi +# } - if [ ${#_optionValues[@]} -gt 1 ]; then - local_echo "$1" - local_echo "--- Select by Name ---" - o=0 - for name in ${optionNames[@]}; do - local_echo "$((++o))) $name" +# ----------------------------------------------------------- +# Prompt user for input and save value to variable +# @param $1 prompt message +# @param $2 default value +# @param $3 variable that is receiving the value +# @param $4 hide text +# @param $5 filter +# ----------------------------------------------------------- +function require_input(){ + + if [ ! -z "$5" ]; then + declare -a option_keys=() + declare -a option_values=() + number_of_tagged_resources=0 + default_tagged_resource="" + o=0 + for key in "${!RESOURCE_MAP[@]}"; do + if [[ $key = *"$5"* ]]; then + # if resource is tagged + option_keys[o]=$key + local tagged=$(echo ${RESOURCE_MAP[$key]} | cut -d, -f2) + local name=$(echo ${RESOURCE_MAP[$key]} | cut -d, -f1) + if [ $tagged == "true" ]; then + option_values[o]="$(tput setaf 3)$name ($key)$(tput sgr0)" + default_tagged_resource=$key + ((number_of_tagged_resources++)) + else + option_values[o]="$name ($key)" + fi + fi + ((o++)) + done + + if [ $number_of_tagged_resources -eq 1 ] && [ "$force" == "true" ]; then + read -r $3 <<< $default_tagged_resource + return 0 + elif [ $number_of_tagged_resources -gt 0 ]; then + selectedValue="" + select option in ${option_values[@]}; do + selectedValue=$option + break done - local_echo "--- Select by Resource ID ---" - select option in ${_optionValues[@]}; - do - read -r $3 <<< $option - return 0 - done - elif [ ${#_optionValues[@]} -eq 1 ]; then - read -r $3 <<< ${_optionValues[0]} - else - if [ "$4" == "true" ]; then - read -e -s -p "$1" -i "$2" $3 - # print new line after read -s - echo + # workaround cant access option_keys from within select + read -r $3 <<< $(echo $selectedValue | sed 's/.*(\(.*\)).*/\1/') + return 0 else - read -e -p "$1" -i "$2" $3 + read_input_normal "$1" "$2" $3 $4 + return 0 fi + + else + read_input_normal "$1" "$2" $3 $4 + fi +} + +function read_input_normal(){ + # if hide input + if [ "$4" == "true" ]; then + read -e -s -p "$1" -i "$2" $3 + # print new line after read -s + echo + else + read -e -p "$1" -i "$2" $3 fi } @@ -55,23 +121,15 @@ function require_input(){ # @param $4 prompt message # @param $5 true if optional # @param $6 true if text input be hidden -# @param $7 array of resource names -# @param $8 array of resource arns +# @param $7 filter # ----------------------------------------------------------- function require_variable(){ - declare -a optionNames=() - declare -a optionValues=() - if [ ! -z "$7" ] && [ ! -z "$8" ]; then - optionNames=("${!7}") - optionValues=("${!8}") - fi - # if parameter is empty if [ -z "$1" ]; then # if required variable value is empty if [ -z "${!2}" ]; then # if force is enabled - if [ "$force" = true ]; then + if [ "$force" = true ] && [ -z "$7" ]; then # if variable is optional if [ "$5" == "true" ]; then # variable should be empty @@ -83,15 +141,15 @@ function require_variable(){ return 0 fi fi - # if force not enabled + # if force not enabled or filter is set # if variable value is optional, then value of "" is possible if [ "$5" == "true" ]; then - require_input "$4" "$3" $2 $6 optionNames[@] optionValues[@] + require_input "$4" "$3" $2 $6 $7 else # if variable value is not optional, repeat until value is set for variable while [[ -z "${!2}" ]] do - require_input "$4" "$3" $2 $6 optionNames[@] optionValues[@] + require_input "$4" "$3" $2 $6 $7 done fi fi @@ -131,7 +189,7 @@ function update_resources(){ tagged="false" fi - RESOURCE_MAP[$arn]="${name:-"Unnamed"}, $tagged" + RESOURCE_MAP[$arn]="${name:-"Unnamed"},$tagged" done typeset -p RESOURCE_MAP >> $resources_file @@ -210,8 +268,8 @@ function require_load_balancer(){ require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] } -function require_instance(){ - require_variable "" instance "" "$instance_ask_message" "false" "false" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] +function require_instance_arn(){ + require_variable "" instance_arn "" "$instance_ask_message" "false" "false" "instance" } function require_region(){ diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh index 3a8468cc3ec..11e603f9b9e 100644 --- a/configuration/aws-automation/lib/resources-eu-west-2.sh +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -13,4 +13,4 @@ declare -- OLD_RESOURCE_JSON="{\"ResourceARN\":\"arn:aws:acm:eu-west-2:017363970 {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"},{\"Value\":\"appSecurityGroup\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff\",\"Tags\":[{\"Value\":\"test\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]}" -declare -A RESOURCE_MAP=([arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb]="MongoDB, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680]="123, false" [arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d]="Unnamed, true" [arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c]="Unnamed, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee]="appSecurityGroup, true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562]="yxc, false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff]="test, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f]="12, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9]="123, false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c]="Multi Instance, true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916]="Database and Messaging, false" ) +declare -A RESOURCE_MAP=([arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb]="MongoDB,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680]="123,false" [arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d]="Unnamed,true" [arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee]="appSecurityGroup,true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562]="yxc,false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff]="test,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f]="12,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c]="Multi Instance,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916]="Database and Messaging,false" ) From 562749bae8bd0ca5d5422b2c3ca35d16a5ddf9e0 Mon Sep 17 00:00:00 2001 From: Radeck Date: Wed, 14 Feb 2018 05:26:37 +0000 Subject: [PATCH 139/214] Generic instance association: choose instance and load balancer --- configuration/aws-automation/aws-setup.sh | 27 ++++---- .../aws-automation/lib/functions_app.sh | 8 +++ .../aws-automation/lib/functions_io.sh | 68 ++++--------------- .../lib/scenario_associate_alb.sh | 10 +-- 4 files changed, 41 insertions(+), 72 deletions(-) diff --git a/configuration/aws-automation/aws-setup.sh b/configuration/aws-automation/aws-setup.sh index 60c51646695..aebd8831965 100755 --- a/configuration/aws-automation/aws-setup.sh +++ b/configuration/aws-automation/aws-setup.sh @@ -80,24 +80,23 @@ if is_exists ${region_config_file}; then fi init_resources -require_instance_arn -safeExit -if $instance; then + +if $instance_scenario; then instance_start safeExit fi -if $shared_instance; then +if $shared_instance_scenario; then shared_instance_start safeExit fi -if $master_instance; then +if $master_instance_scenario; then master_instance_start safeExit fi -if $associate_alb; then +if $associate_alb_scenario; then associate_alb_start safeExit fi @@ -205,10 +204,10 @@ unset options [[ $# -eq 0 ]] && set -- "--help" # Set default value of variable without parameter value to false -associate_alb=false -instance=false -shared_instance=false -master_instance=false +associate_alb_scenario=false +instance_scenario=false +shared_instance_scenario=false +master_instance_scenario=false # Read the options and set variables while [[ $1 = -?* ]]; do @@ -234,10 +233,10 @@ while [[ $1 = -?* ]]; do -e|--contact-email) shift; contact_email_param=${1} ;; -f|--force) force=true ;; -d|--debug) debug=true ;; - --instance) instance=true ;; - --shared-instance) shared_instance=true ;; - --master-instance) master_instance=true ;; - --associate-alb) associate_alb=true ;; + --instance) instance_scenario=true ;; + --shared-instance) shared_instance_scenario=true ;; + --master-instance) master_instance_scenario=true ;; + --associate-alb) associate_alb_scenario=true ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; esac diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index abcc7cf8266..620a90707b4 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -129,6 +129,14 @@ function append_macro_to_001_events_conf(){ ssh_wrapper $3@$4 echo "$patched_content >> $events_conf" } +function reload_httpd(){ + local_echo "Reloading httpd..." + ssh_wrapper $1@$2 "apachectl configtest >/dev/null 2>&1" + out=$(ssh_wrapper $1@$2 "/etc/init.d/httpd reload") +} + + + # ----------------------------------------------------------- # Wait until 001_events.conf file appears on instance # @param $1 ssh user diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index af13e8b5c81..14e0453bd2f 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -1,47 +1,10 @@ #!/usr/bin/env bash -# ----------------------------------------------------------- -# Prompt user for input and save value to variable -# @param $1 prompt message -# @param $2 default value -# @param $3 variable that is receiving the value -# @param $4 hide text -# @param $5 array of resource names -# @param $6 array of resource arns -# ----------------------------------------------------------- -# function require_input(){ -# declare -a _optionNames=() -# declare -a _optionValues=() -# if [ ! -z "$5" ] && [ ! -z "$6" ]; then -# _optionNames=("${!5}") -# _optionValues=("${!6}") -# fi -# -# if [ ${#_optionValues[@]} -gt 1 ]; then -# local_echo "$1" -# local_echo "--- Select by Name ---" -# o=0 -# for name in ${optionNames[@]}; do -# local_echo "$((++o))) $name" -# done -# local_echo "--- Select by Resource ID ---" -# select option in ${_optionValues[@]}; -# do -# read -r $3 <<< $option -# return 0 -# done -# elif [ ${#_optionValues[@]} -eq 1 ]; then -# read -r $3 <<< ${_optionValues[0]} -# else -# if [ "$4" == "true" ]; then -# read -e -s -p "$1" -i "$2" $3 -# # print new line after read -s -# echo -# else -# read -e -p "$1" -i "$2" $3 -# fi -# fi -# } +CHOOSE_FROM_INSTANCES=":instance" +CHOOSE_FROM_SECURITY_GROUPS=":security-group" +CHOOSE_FROM_IMAGES=":image" +CHOOSE_FROM_CERTIFICATES=":certificate" +CHOOSE_FROM_LOAD_BALANCERS=":loadbalancer" # ----------------------------------------------------------- # Prompt user for input and save value to variable @@ -76,7 +39,7 @@ function require_input(){ ((o++)) done - if [ $number_of_tagged_resources -eq 1 ] && [ "$force" == "true" ]; then + if [ $number_of_tagged_resources -eq 1 ] && [ "$force" == "true" ] || [ ${#option_keys[@]} -eq 1 ]; then read -r $3 <<< $default_tagged_resource return 0 elif [ $number_of_tagged_resources -gt 0 ]; then @@ -156,6 +119,7 @@ function require_variable(){ else # if parameter is not empty, read its value into required variable read -r "$2" <<< "$1" + return 0 fi } @@ -253,7 +217,7 @@ function get_config_variable(){ } function require_image_id(){ - require_variable "" image_id "" "$image_id_ask_message" "false" "false" TAGGED_IMAGE_NAMES[@] TAGGED_IMAGE_ARNS[@] "" + require_variable "" image_id "" "$image_id_ask_message" "false" "false" $CHOOSE_FROM_IMAGES } function require_event_name(){ @@ -261,15 +225,15 @@ function require_event_name(){ } function require_instance_security_group_id(){ - require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" TAGGED_SECURITY_GROUP_NAMES[@] TAGGED_SECURITY_GROUP_ARNS[@] + require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" $CHOOSE_FROM_SECURITY_GROUPS } function require_load_balancer(){ - require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" TAGGED_LOADBALANCER_NAMES[@] TAGGED_LOADBALANCER_ARNS[@] + require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" $CHOOSE_FROM_LOAD_BALANCERS } -function require_instance_arn(){ - require_variable "" instance_arn "" "$instance_ask_message" "false" "false" "instance" +function require_instance(){ + require_variable "" instance "" "$instance_ask_message" "false" "false" $CHOOSE_FROM_INSTANCES } function require_region(){ @@ -316,16 +280,12 @@ function require_user_password(){ fi } -function require_public_dns_name(){ - require_variable "$public_dns_name_param" public_dns_name "" "$public_dns_name_ask_message" "false" "false" -} - function require_ssh_user(){ require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" "false" "false" } function require_super_instance(){ - require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" TAGGED_INSTANCE_NAMES[@] TAGGED_INSTANCE_ARNS[@] + require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" $CHOOSE_FROM_INSTANCES } function require_description(){ @@ -341,7 +301,7 @@ function require_contact_email(){ } function require_build_version(){ - require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" + require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" "false" } instance_ask_message="Please enter an instance to use: " diff --git a/configuration/aws-automation/lib/scenario_associate_alb.sh b/configuration/aws-automation/lib/scenario_associate_alb.sh index f7122ea1e7c..2b32dc9e51a 100644 --- a/configuration/aws-automation/lib/scenario_associate_alb.sh +++ b/configuration/aws-automation/lib/scenario_associate_alb.sh @@ -18,6 +18,7 @@ function associate_alb_require(){ require_instance require_instance_short_name require_load_balancer + require_key_file require_ssh_user disable_aws_success_output @@ -29,10 +30,10 @@ function associate_alb_require(){ function associate_alb_execute() { # create target group for instance local target_group_arn=$(exit_on_fail create_target_group "S-dedicated-$instance_short_name") - set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "80" "5" "4" "2" "2" + exit_on_fail set_target_group_health_check "$target_group_arn" "HTTP" "/index.html" "80" "5" "4" "2" "2" - listener_arn=$(get_first_https_listener )# add instance to target group - register_targets $target_group_arn $instance_id + listener_arn=$(exit_on_fail get_first_https_listener $load_balancer) + exit_on_fail register_targets $target_group_arn $instance_id # create rule for host-based forwarding within alb local domain=$(exit_on_fail create_rule $listener_arn $instance_short_name $target_group_arn) @@ -40,5 +41,6 @@ function associate_alb_execute() { header "Apache configuration" # configure apache with event macro - append_macro_to_001_events_conf $domain $event_id $ssh_user $public_dns_name "8888" + exit_on_fail append_macro_to_001_events_conf $domain "" $ssh_user $public_dns_name "8888" + exit_on_fail reload_httpd $ssh_user $public_dns_name } From 3391ec8ea98006fa12bd859d0be9b7f484d254b8 Mon Sep 17 00:00:00 2001 From: Leon Radeck Date: Wed, 14 Feb 2018 07:46:29 +0100 Subject: [PATCH 140/214] Bug fixes --- .../aws-automation/lib/functions_app.sh | 4 +- .../aws-automation/lib/functions_elb.sh | 4 +- .../aws-automation/lib/functions_io.sh | 114 +----------------- .../aws-automation/lib/functions_wrapper.sh | 31 ++--- .../aws-automation/lib/require_variables.sh | 108 +++++++++++++++++ .../aws-automation/lib/resources-eu-west-2.sh | 6 +- .../aws-automation/lib/scenario_instance.sh | 19 ++- .../lib/scenario_shared_instance.sh | 25 ++-- configuration/aws-automation/lib/variables.sh | 12 -- 9 files changed, 157 insertions(+), 166 deletions(-) create mode 100644 configuration/aws-automation/lib/require_variables.sh diff --git a/configuration/aws-automation/lib/functions_app.sh b/configuration/aws-automation/lib/functions_app.sh index 620a90707b4..b790b003c72 100644 --- a/configuration/aws-automation/lib/functions_app.sh +++ b/configuration/aws-automation/lib/functions_app.sh @@ -32,7 +32,7 @@ function get_latest_release(){ # ----------------------------------------------------------- function get_access_token(){ local_echo -n "Getting access token..." - curl_until_http_200 http://$1:$2@$3:$4/security/api/restsecurity/access_token + curl_until_response "200" http://$1:$2@$3:$4/security/api/restsecurity/access_token curl_wrapper -X GET "http://$1:$2@$3:$4/security/api/restsecurity/access_token" | get_attribute '.access_token' } @@ -46,7 +46,7 @@ function get_access_token(){ # ----------------------------------------------------------- function create_event(){ local_echo -n "Creating event with name $4..." - curl_until_http_401 http://$2:$3/sailingserver/api/v1/events/createEvent + curl_until_response "401" http://$2:$3/sailingserver/api/v1/events/createEvent curl_wrapper -X POST -H "Authorization: Bearer $1" "http://$2:$3/sailingserver/api/v1/events/createEvent" --data "eventName=$4" --data "venuename=Default" --data "createregatta=false" | get_attribute '.eventid' } diff --git a/configuration/aws-automation/lib/functions_elb.sh b/configuration/aws-automation/lib/functions_elb.sh index 9d192fd22d5..710681e88ee 100644 --- a/configuration/aws-automation/lib/functions_elb.sh +++ b/configuration/aws-automation/lib/functions_elb.sh @@ -9,8 +9,8 @@ function create_target_group(){ local_echo "Creating target group $1..." local target_group_name=$(only_letters_numbers_dash $1) local vpc_id=$(get_default_vpc_id) - aws_wrapper elbv2 create-target-group --name $target_group_name --protocol "$target_group_protocol" \ - --port $target_group_port --vpc-id $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn' + aws_wrapper elbv2 create-target-group --name $target_group_name --protocol "HTTPS" \ + --port "443" --vpc-id $vpc_id | get_attribute '.TargetGroups[0].TargetGroupArn' } # ----------------------------------------------------------- diff --git a/configuration/aws-automation/lib/functions_io.sh b/configuration/aws-automation/lib/functions_io.sh index 14e0453bd2f..8cf20fbc65a 100644 --- a/configuration/aws-automation/lib/functions_io.sh +++ b/configuration/aws-automation/lib/functions_io.sh @@ -177,7 +177,7 @@ function init_resources(){ if ! is_exists ~/.aws-automation/config; then local_echo "Creating ~/.aws-automation/config..." - create_configuration + create_global_configuration fi } @@ -196,7 +196,7 @@ function create_region_configuration(){ echo -e $config > ~/.aws-automation/config-$region } -function create_configuration(){ +function create_global_configuration(){ mkdir -p ~/.aws-automation touch ~/.aws-automation/config @@ -215,113 +215,3 @@ function set_config_variable(){ function get_config_variable(){ ./lib/build-config.sh --bash ~/.aws-automation/config-$region "$1" } - -function require_image_id(){ - require_variable "" image_id "" "$image_id_ask_message" "false" "false" $CHOOSE_FROM_IMAGES -} - -function require_event_name(){ - require_variable "$event_name_param" event_name "" "$event_name_ask_message" "true" "false" -} - -function require_instance_security_group_id(){ - require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" $CHOOSE_FROM_SECURITY_GROUPS -} - -function require_load_balancer(){ - require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" $CHOOSE_FROM_LOAD_BALANCERS -} - -function require_instance(){ - require_variable "" instance "" "$instance_ask_message" "false" "false" $CHOOSE_FROM_INSTANCES -} - -function require_region(){ - require_variable "$region_param" region "$default_region" "$region_ask_message" "false" "false" -} - -function require_instance_type(){ - require_variable "$instance_type_param" instance_type "$default_instance_type" "$instance_type_ask_message" "false" "false" - -} -function require_instance_name(){ - require_variable "$instance_name_param" instance_name "$default_instance_name" "$instance_name_ask_message" "false" "false" -} - -function require_instance_short_name(){ - require_variable "$instance_short_name_param" instance_short_name "$default_instance_short_name" "$instance_short_name_ask_message" "false" "false" -} - -function require_key_name(){ - if [ -z ${key_file+x} ] || [ ! -z "$key_file" ]; then - require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "true" "false" - fi -} - -function require_key_file(){ - if [ -z ${key_name+x} ] || [ ! -z "$key_name" ]; then - require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "true" "false" - fi -} - -function require_new_admin_password(){ - require_variable "$new_admin_password_param" new_admin_password "$default_new_admin_password" "$new_admin_password_ask_message" "true" "true" -} - -function require_user_username(){ - if [ ! -z ${user_password+x} ] || [ ! -z "$user_password" ]; then - require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" "true" "false" - fi -} - -function require_user_password(){ - if [ ! -z ${user_name+x} ] || [ ! -z "$user_name" ]; then - require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" - fi -} - -function require_ssh_user(){ - require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" "false" "false" -} - -function require_super_instance(){ - require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" $CHOOSE_FROM_INSTANCES -} - -function require_description(){ - require_variable "$description_param" description "" "$description_message" "true" "false" -} - -function require_contact_person(){ - require_variable "$contact_person_param" contact_person "$default_contact_person" "$contact_person_message" "true" "false" -} - -function require_contact_email(){ - require_variable "$contact_email_param" contact_email "$default_contact_email" "$contact_email_message" "true" "false" -} - -function require_build_version(){ - require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" "false" -} - -instance_ask_message="Please enter an instance to use: " -event_name_ask_message="Please enter an event name (leave blank to skip event creation):" -instance_security_group_id_ask_message="Please select the security group for the instance: " -load_balancer_ask_message="Please select/enter the load balancer dns name: " -region_ask_message="Please enter the region you want to use (e.g. eu-west-2): " -instance_type_ask_message="Please enter the instance type (e.g. t2.medium): " -key_name_ask_message="Please enter the name of your keypair to connect to the instance (e.g. leon-keypair). Leave blank to use ~/.ssh key: " -instance_name_ask_message="Please enter a name for the instance: (e.g \"WC Santander 2017\"): " -instance_short_name_ask_message="Please enter a short name for the instance (e.g. \"wcs17\"): " -key_file_ask_message="Please enter the file path of the keypair or leave empty to use ~/.ssh key: " -new_admin_password_ask_message="Please enter a new password for the admin user (leave blank to skip password change): " -mongo_db_host_ask_message="Please enter the ip adress of the mongo db server: " -mongo_db_port_ask_message="Please enter the port of the mongo db server: " -user_username_ask_message="Please enter the username of your new user (leave blank to skip user creation): " -user_password_ask_message="Please enter the password of your new user (leave blank to skip user creation): " -ssh_user_ask_message="Please enter the ssh user to connect to the instance: " -super_instance_message="Please select/enter the dns name of the superior instance: " -description_message="Please enter a description for the server (leave blank to skip): " -contact_person_message="Please enter the name of a contact person (leave blank to skip): " -contact_email_message="Please enter the email of the contact person (leave blank to skip): " -build_version_message="Please enter a build version to use (releases.sapsailing.com): " diff --git a/configuration/aws-automation/lib/functions_wrapper.sh b/configuration/aws-automation/lib/functions_wrapper.sh index 4e784b63539..37d267f22c5 100644 --- a/configuration/aws-automation/lib/functions_wrapper.sh +++ b/configuration/aws-automation/lib/functions_wrapper.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash declare -g DISPLAY_SUCCESS_FOR_THIS_COMMAND="true" +HTTP_RETRY_INTERVAL=5 +TIMEOUT=300 + # ----------------------------------------------------------- # Does the error checking for curl commands. # Captures response and http code of curl command into variable $out. @@ -79,30 +82,16 @@ function ssh_wrapper(){ # ----------------------------------------------------------- # Executes curl command until status code is 200 # ----------------------------------------------------------- -function curl_until_http_200(){ +function curl_until_response(){ + response_code=$1 + shift start_time="$(date -u +%s)" - while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "200" ]]; + while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $HTTP_RETRY_INTERVAL "$@") != "$response_code" ]]; do fail_on_timeout $start_time local_echo -n "." - sleep $http_retry_interval; - done - local_echo "" -} - -# ----------------------------------------------------------- -# Executes curl command until status code is 401. -# TODO: Fix code duplication (curl_until_http_200, curl_until_http_401) -# ----------------------------------------------------------- -function curl_until_http_401(){ - start_time="$(date -u +%s)" - while [[ $(curl -s -o /dev/null -w ''%{http_code}'' --connect-timeout $http_retry_interval "$@") != "401" ]]; - do - fail_on_timeout $start_time - - local_echo -n "." - sleep $http_retry_interval; + sleep $HTTP_RETRY_INTERVAL; done local_echo "" } @@ -114,13 +103,13 @@ function do_until_true(){ fail_on_timeout $start_time echo -n . - sleep $http_retry_interval + sleep $HTTP_RETRY_INTERVAL done } function fail_on_timeout(){ end_time="$(date -u +%s)" - if [ "$(($end_time-$1))" -gt 180 ]; then + if [ "$(($end_time-$1))" -gt $TIMEOUT ]; then error "TIMEOUT" safeExit fi diff --git a/configuration/aws-automation/lib/require_variables.sh b/configuration/aws-automation/lib/require_variables.sh new file mode 100644 index 00000000000..5e93cc784c9 --- /dev/null +++ b/configuration/aws-automation/lib/require_variables.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +instance_ask_message="Please enter an instance to use: " +event_name_ask_message="Please enter an event name (leave blank to skip event creation):" +instance_security_group_id_ask_message="Please select the security group for the instance: " +load_balancer_ask_message="Please select/enter the load balancer dns name: " +region_ask_message="Please enter the region you want to use (e.g. eu-west-2): " +instance_type_ask_message="Please enter the instance type (e.g. t2.medium): " +key_name_ask_message="Please enter the name of your keypair to user for the instance (e.g. leon-keypair). " +instance_name_ask_message="Please enter a name for the instance: (e.g \"WC Santander 2017\"): " +instance_short_name_ask_message="Please enter a short name for the instance (e.g. \"wcs17\"): " +key_file_ask_message="Please enter the file path of the keypair or leave empty to use ~/.ssh key: " +new_admin_password_ask_message="Please enter a new password for the admin user (leave blank to skip password change): " +mongo_db_host_ask_message="Please enter the ip adress of the mongo db server: " +mongo_db_port_ask_message="Please enter the port of the mongo db server: " +user_username_ask_message="Please enter the username of your new user (leave blank to skip user creation): " +user_password_ask_message="Please enter the password of your new user (leave blank to skip user creation): " +ssh_user_ask_message="Please enter the ssh user to connect to the instance: " +super_instance_message="Please select/enter the dns name of the superior instance: " +description_message="Please enter a description for the server (leave blank to skip): " +contact_person_message="Please enter the name of a contact person (leave blank to skip): " +contact_email_message="Please enter the email of the contact person (leave blank to skip): " +build_version_message="Please enter a build version to use (releases.sapsailing.com): " + + +function require_image_id(){ + require_variable "" image_id "" "$image_id_ask_message" "false" "false" $CHOOSE_FROM_IMAGES +} + +function require_event_name(){ + require_variable "$event_name_param" event_name "" "$event_name_ask_message" "true" "false" +} + +function require_instance_security_group_id(){ + require_variable "" instance_security_group_id "" "$instance_security_group_id_ask_message" "false" "false" $CHOOSE_FROM_SECURITY_GROUPS +} + +function require_load_balancer(){ + require_variable "" load_balancer "" "$load_balancer_ask_message" "false" "false" $CHOOSE_FROM_LOAD_BALANCERS +} + +function require_instance(){ + require_variable "" instance "" "$instance_ask_message" "false" "false" $CHOOSE_FROM_INSTANCES +} + +function require_region(){ + require_variable "$region_param" region "$default_region" "$region_ask_message" "false" "false" +} + +function require_instance_type(){ + require_variable "$instance_type_param" instance_type "$default_instance_type" "$instance_type_ask_message" "false" "false" + +} +function require_instance_name(){ + require_variable "$instance_name_param" instance_name "$script_start_time" "$instance_name_ask_message" "false" "false" +} + +function require_instance_short_name(){ + require_variable "$instance_short_name_param" instance_short_name "$timestamp" "$instance_short_name_ask_message" "false" "false" +} + +function require_key_name(){ + require_variable "$key_name_param" key_name "$default_key_name" "$key_name_ask_message" "false" "false" +} + +function require_key_file(){ + require_variable "$key_file_param" key_file "$default_key_file" "$key_file_ask_message" "true" "false" +} + +function require_new_admin_password(){ + require_variable "$new_admin_password_param" new_admin_password "$default_new_admin_password" "$new_admin_password_ask_message" "true" "true" +} + +function require_user_username(){ + if [ ! -z ${user_password+x} ] || [ ! -z "$user_password" ]; then + require_variable "$user_username_param" user_username "$default_user_username" "$user_username_ask_message" "true" "false" + fi +} + +function require_user_password(){ + if [ ! -z ${user_name+x} ] || [ ! -z "$user_name" ]; then + require_variable "$user_password_param" user_password "$default_user_password" "$user_password_ask_message" "true" "true" + fi +} + +function require_ssh_user(){ + require_variable "$ssh_user_param" ssh_user "$default_ssh_user" "$ssh_user_ask_message" "false" "false" +} + +function require_super_instance(){ + require_variable "$super_instance_param" super_instance "$default_super_instance" "$super_instance_message" "false" "false" $CHOOSE_FROM_INSTANCES +} + +function require_description(){ + require_variable "$description_param" description "" "$description_message" "true" "false" +} + +function require_contact_person(){ + require_variable "$contact_person_param" contact_person "$default_contact_person" "$contact_person_message" "true" "false" +} + +function require_contact_email(){ + require_variable "$contact_email_param" contact_email "$default_contact_email" "$contact_email_message" "true" "false" +} + +function require_build_version(){ + require_variable "$build_version_param" build_version "$latest_release" "$build_version_message" "false" "false" +} diff --git a/configuration/aws-automation/lib/resources-eu-west-2.sh b/configuration/aws-automation/lib/resources-eu-west-2.sh index 11e603f9b9e..bb4afed6857 100644 --- a/configuration/aws-automation/lib/resources-eu-west-2.sh +++ b/configuration/aws-automation/lib/resources-eu-west-2.sh @@ -1,7 +1,11 @@ declare -- OLD_RESOURCE_JSON="{\"ResourceARN\":\"arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-02abdf10071ce9ce6\",\"Tags\":[{\"Value\":\"2018-02-14 06:57:30\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-05f2fda5ce72c2b36\",\"Tags\":[{\"Value\":\"2018-02-14 07:22:33\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f\",\"Tags\":[{\"Value\":\"12\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-08803ecb16560b18a\",\"Tags\":[{\"Value\":\"2018-02-14 07:15:26\",\"Key\":\"Name\"}]} +{\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-08e4a99e69f6d8dce\",\"Tags\":[{\"Value\":\"2018-02-14 07:07:20\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3\",\"Tags\":[{\"Value\":\"123\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb\",\"Tags\":[{\"Value\":\"MongoDB\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562\",\"Tags\":[{\"Value\":\"yxc\",\"Key\":\"Name\"}]} @@ -13,4 +17,4 @@ declare -- OLD_RESOURCE_JSON="{\"ResourceARN\":\"arn:aws:acm:eu-west-2:017363970 {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"},{\"Value\":\"appSecurityGroup\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff\",\"Tags\":[{\"Value\":\"test\",\"Key\":\"Name\"}]} {\"ResourceARN\":\"arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a\",\"Tags\":[{\"Value\":\"true\",\"Key\":\"AutoDiscover\"}]}" -declare -A RESOURCE_MAP=([arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb]="MongoDB,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680]="123,false" [arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d]="Unnamed,true" [arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee]="appSecurityGroup,true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562]="yxc,false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff]="test,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f]="12,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c]="Multi Instance,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916]="Database and Messaging,false" ) +declare -A RESOURCE_MAP=([arn:aws:ec2:eu-west-2:017363970217:instance/i-096a32ca8c28bedbb]="MongoDB,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-08803ecb16560b18a]="2018-02-14 07:15:26,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-04eeb1346463540c5]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-09330d7744ebef9b3]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-02abdf10071ce9ce6]="2018-02-14 06:57:30,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f2e26f9a7860e680]="123,false" [arn:aws:acm:eu-west-2:017363970217:certificate/d2ae17b0-75ed-4b56-ac12-a80f9e5c493d]="Unnamed,true" [arn:aws:elasticloadbalancing:eu-west-2:017363970217:loadbalancer/app/DummyALB/da70f61a914fc50a]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:image/ami-39f3e25d]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-05f2fda5ce72c2b36]="2018-02-14 07:22:33,false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-74c92d1c]="Unnamed,true" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-871732ee]="appSecurityGroup,true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0a65849491dfc8562]="yxc,false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-9732d6ff]="test,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-068b644ad5fbe6e5f]="12,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0f5308bf09c42d7f9]="123,false" [arn:aws:ec2:eu-west-2:017363970217:instance/i-0c57f03afc68de24c]="Multi Instance,true" [arn:aws:ec2:eu-west-2:017363970217:instance/i-08e4a99e69f6d8dce]="2018-02-14 07:07:20,false" [arn:aws:ec2:eu-west-2:017363970217:security-group/sg-7f0c2916]="Database and Messaging,false" ) diff --git a/configuration/aws-automation/lib/scenario_instance.sh b/configuration/aws-automation/lib/scenario_instance.sh index 4139d821e7c..7cd5d1255e1 100644 --- a/configuration/aws-automation/lib/scenario_instance.sh +++ b/configuration/aws-automation/lib/scenario_instance.sh @@ -5,6 +5,7 @@ function instance_start(){ instance_require + instance_preview instance_execute } @@ -30,6 +31,19 @@ function instance_require(){ require_user_password } +function instance_preview(){ + header "Preview" + user_data=$(build_user_data) + local_echo "An instance will be created with the following specifications: " + local_echo "To be implemented..." + seek_confirmation "Do you want to continue?" + if is_confirmed; then + break + else + break + fi +} + # ----------------------------------------------------------- # Execute instance scenario # @param $1 user data @@ -38,7 +52,6 @@ function instance_execute() { header "Instance Initialization" - local user_data=$(build_user_data) instance_id=$(create_instance "$user_data") public_dns_name=$(get_public_dns_name $instance_id) wait_for_ssh_connection $ssh_user $public_dns_name @@ -47,6 +60,10 @@ function instance_execute() { local port="8888" configure_application $public_dns_name $port $event_name $new_admin_password $user_username $user_pass + + header "Conclusion" + + success "Instance should be reachable through $public_dns_name." } function build_user_data(){ diff --git a/configuration/aws-automation/lib/scenario_shared_instance.sh b/configuration/aws-automation/lib/scenario_shared_instance.sh index a0e232c7701..78bd09482ec 100644 --- a/configuration/aws-automation/lib/scenario_shared_instance.sh +++ b/configuration/aws-automation/lib/scenario_shared_instance.sh @@ -26,7 +26,7 @@ function shared_instance_require(){ require_new_admin_password require_user_username require_user_password - + require_ssh_user require_contact_person require_contact_email require_description @@ -46,8 +46,8 @@ function shared_instance_check_preconditions(){ local instance_id=$(get_instance_id $super_instance) - echo "Checking if instance has tag: 'ssh_user'." - ssh_user=$(exit_on_fail get_tag_value_for_key $instance_id "ssh_user") + # echo "Checking if instance has tag: 'ssh_user'." + # ssh_user=$(exit_on_fail get_tag_value_for_key $instance_id "ssh_user") echo "Checking if directory $server_dir exists already..." exit_on_fail execute_remote "[ ! -d $server_dir ]" @@ -55,10 +55,9 @@ function shared_instance_check_preconditions(){ echo "Checking if ssh connection $ssh_user@$super_instance is working..." exit_on_fail execute_remote ls - echo "Checking if super instance has user data.\ - The instance should not have any user data set, otherwise the refreshInstance script will not work properly." - - exit_on_fail [ $(get_user_data_from_instance $instance_id) == "None" ] + # echo "Checking if super instance has user data.\ + # The instance should not have any user data set, otherwise the refreshInstance script will not work properly." + # exit_on_fail [ $(get_user_data_from_instance $instance_id) == "None" ] } function shared_instance_execute() { @@ -75,13 +74,13 @@ function shared_instance_execute() { exit_on_fail execute_remote mkdir $server_dir local_echo "Getting next unused SERVER_PORT..." - local server_port=$(find_first_unused_port "SERVER_PORT" $default_server_port) + local server_port=$(find_first_unused_port "SERVER_PORT" "8888") local_echo "Getting next unused TELNET_PORT..." - local telnet_port=$(find_first_unused_port "TELNET_PORT" $default_telnet_port) + local telnet_port=$(find_first_unused_port "TELNET_PORT" "14900") local_echo "Getting next unused EXPEDITION_PORT..." - local expedition_port=$(find_first_unused_port "EXPEDITION_PORT" $default_expedition_port) + local expedition_port=$(find_first_unused_port "EXPEDITION_PORT" "2000") # copy refreshInstance.sh from /servers/server to sub instance directory local_echo "Copying $refreshInstance_file to $server_dir..." @@ -129,7 +128,7 @@ function shared_instance_execute() { header "Event and user creation" - event_id=$(configure_application $super_instance $port $event_name $new_admin_password $user_username $user_password) + event_id=$(configure_application $super_instance $server_port $event_name $new_admin_password $user_username $user_password) header "Configuring ALB" @@ -146,10 +145,6 @@ function shared_instance_execute() { execute_remote_root "echo -e \"\n# $instance_short_name (${description:-"Unknown"}, ${contact_person:-"Unknown"}, ${contact_email:-"Unknown"})\" >> $events_conf" append_macro_to_001_events_conf "$domain" "$event_id" "root" "$super_instance" "$server_port" - local_echo "Reloading httpd..." - execute_remote_root "apachectl configtest >/dev/null 2>&1" - out=$(execute_remote_root "/etc/init.d/httpd reload") - header "Conclusion" success "Sub instance should be reachable through https://$domain." diff --git a/configuration/aws-automation/lib/variables.sh b/configuration/aws-automation/lib/variables.sh index 5cf6e6a3d21..976004b10bf 100644 --- a/configuration/aws-automation/lib/variables.sh +++ b/configuration/aws-automation/lib/variables.sh @@ -1,18 +1,6 @@ #!/usr/bin/env bash - latest_release=$(get_latest_release) -autoDiscoverResourceTagName='AutoDiscover' -target_group_protocol='HTTPS' -target_group_port='443' -ssh_retry_interval=2 -http_retry_interval=5 -default_instance_name="$script_start_time" -default_instance_short_name="$timestamp" events_conf='/etc/httpd/conf.d/001-events.conf' -default_server_port='8888' -default_telnet_port='14900' -default_expedition_port='2000' - scriptName=`basename $0` #Set Script Name variable scriptBasename="$(basename ${scriptName} .sh)" # Strips '.sh' from scriptName declare -r script_start_time=$(LC_ALL=C date +"%Y-%m-%d %H:%M:%S") # Returns: 2015-06-15 22:34:40 From 3a4a5be2fd03558221d20d0316be8c491fea0444 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 14 Feb 2018 12:24:12 +0100 Subject: [PATCH 141/214] bug1301 Replaced former WindFinder PNG images with a SVG --- .../shared/partials/windfinder/Windfinder.gss | 28 +++++++++++------- .../windfinder/WindfinderControl.java | 4 ++- .../windfinder/WindfinderControl.ui.xml | 2 +- .../windfinder/WindfinderLaunchPadItem.java | 4 ++- .../windfinder/WindfinderLaunchPadItem.ui.xml | 4 +-- .../windfinder/WindfinderResources.java | 9 ++---- .../windfinder/windfinder-logo-red.png | Bin 787 -> 0 bytes .../windfinder/windfinder-logo-white.png | Bin 854 -> 0 bytes .../partials/windfinder/windfinder-logo.svg | 3 ++ 9 files changed, 31 insertions(+), 23 deletions(-) delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-red.png delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-white.png create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo.svg diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss index 029931cb74c..842e9380598 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/Windfinder.gss @@ -1,5 +1,8 @@ +@def WINFINDER_COLOR_PRIMARY #ffffff; +@def WINFINDER_COLOR_SECONDARY #d0021b; + .windfindercontrol { - background-color: #d0021b; + background-color: WINFINDER_COLOR_SECONDARY; text-transform: uppercase; text-decoration: none; vertical-align: bottom; @@ -11,7 +14,10 @@ } .windfindercontrol_icon { margin: -0.5em 0.25em; - background-image: resourceUrl("windfinderLogoWhite"); +} +.windfindercontrol_icon svg { + display: block; + fill: WINFINDER_COLOR_PRIMARY; } .windfindercontrol:hover { cursor: pointer; @@ -36,21 +42,23 @@ align-items: center; height: 3em; padding: 0 1.333333333333333em 0 0.666666666666667em; - background: #fff; - color: #d0021b; + background: WINFINDER_COLOR_PRIMARY; + color: WINFINDER_COLOR_SECONDARY; font-weight: normal; } .windfinderlaunchpad_content_item:hover { - background-color: #d0021b; - color: #ffffff; + background-color: WINFINDER_COLOR_SECONDARY; + color: WINFINDER_COLOR_PRIMARY; text-decoration: underline; } .windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_icon { margin-right: 1em; - background-image: resourceUrl("windfinderLogoRed"); } -.windfinderlaunchpad_content_item:hover .windfinderlaunchpad_content_item_icon { - background-image: resourceUrl("windfinderLogoWhite"); +.windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_icon svg { + fill: WINFINDER_COLOR_SECONDARY; +} +.windfinderlaunchpad_content_item:hover .windfinderlaunchpad_content_item_icon svg { + fill: WINFINDER_COLOR_PRIMARY; } .windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_title { white-space: nowrap; @@ -58,6 +66,4 @@ .windfindercontrol_icon, .windfinderlaunchpad_content_item .windfinderlaunchpad_content_item_icon { width: 1.666666666666667em; height: 1.666666666666667em; - background-size: contain; - background-repeat: no-repeat; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java index fb2e7b95960..16aa04c0ddb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java @@ -20,7 +20,8 @@ public class WindfinderControl extends Widget { private static WindfinderLaunchPadItemUiBinder uiBinder = GWT.create(WindfinderLaunchPadItemUiBinder.class); - @UiField DivElement labelUi; + @UiField WindfinderResources local_res; + @UiField DivElement labelUi, iconUi; private final WindfinderLaunchPadController launchPadController; private Iterable spotData; @@ -33,6 +34,7 @@ public class WindfinderControl extends Widget { WindfinderResources.INSTANCE.css().ensureInjected(); this.launchPadController = new WindfinderLaunchPadController(windfinderUrlFactory); setElement(uiBinder.createAndBindUi(this)); + this.iconUi.setInnerHTML(local_res.windfinderLogo().getText()); sinkEvents(Event.ONCLICK); this.setVisible(false); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml index 5ed936a8208..f0562513370 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml @@ -5,6 +5,6 @@
Weather Data
-
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java index 527ce311171..4ca39c9601e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.java @@ -14,13 +14,15 @@ class WindfinderLaunchPadItem extends UIObject { interface WindfinderLaunchPadItemUiBinder extends UiBinder { } - @UiField DivElement itemLabelUi; + @UiField WindfinderResources local_res; + @UiField DivElement itemLabelUi, itemIconUi; private final AnchorElement anchorUi; WindfinderLaunchPadItem(String label, String raceViewerUrl) { setElement(anchorUi = uiBinder.createAndBindUi(this)); anchorUi.setHref(raceViewerUrl); itemLabelUi.setInnerText(label); + itemIconUi.setInnerHTML(local_res.windfinderLogo().getText()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml index 90ba87fd341..6e8554eafda 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderLaunchPadItem.ui.xml @@ -1,10 +1,8 @@ - - -
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java index c705fb03825..7c705ff1340 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderResources.java @@ -3,7 +3,7 @@ package com.sap.sailing.gwt.home.shared.partials.windfinder; import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; -import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.resources.client.TextResource; public interface WindfinderResources extends ClientBundle { public static final WindfinderResources INSTANCE = GWT.create(WindfinderResources.class); @@ -11,11 +11,8 @@ public interface WindfinderResources extends ClientBundle { @Source("Windfinder.gss") LocalCss css(); - @Source("windfinder-logo-white.png") - ImageResource windfinderLogoWhite(); - - @Source("windfinder-logo-red.png") - ImageResource windfinderLogoRed(); + @Source("windfinder-logo.svg") + TextResource windfinderLogo(); public interface LocalCss extends CssResource { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-red.png b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo-red.png deleted file mode 100644 index d9c3d0a7075a7542e6919a7a2bcbb45204d8186b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 787 zcmV+u1MK{XP)N2bZe?^J zG%heMF*(%MvSa`N0+2~WK~!i%-Iv>O!yphvGif9L=_OUUhBT1>bd!#hdo`>E(8}5c zguovs)&rJ%yjYIkmSuS<{@y=6!6Wdg4w{u9_XC!|LmfC9L5v-X<#;@I2Ppt@G2y{6 z>cr7O_-A6moPr3Rf6g@4?aA?Kkr82FaMm?p?H<_`8W9AxbS-8MK?L=5P1v?4*%ciH zh74gfiDzuxBT3OwK*&-#GOi%Pm2AnJF$EE^k;Yu~^#ufHG`t=W?7xMzM}K`dA!$9d}$5w8^0#^m%9#9}paWDCpV?A{T51hG)9 z9PtWn9naZasDX=w#k3F}yn<@?hpF8FWQhbW?9;ba!ELWdLwtX>N2bZe?^J zG%heMF*(%MvSa`N0@F!EK~!i%-I|GZn=lYX{r`X1B}vnyY4$xao1hv+P>u8u5@0~S z=Uk6@)=22Y$2N9$P178;FihRz>bfZ(pXA-+++d43wjWGc8Bh09gdCz9Yc_4(^6o(vKlBPJ>3Y}%8#ffz#}2k=d3!HQ6s(^eY|t^V zIoQF*j3)bWN`=kC4z{4!?`0W{GoKTz>{OgNonS@hz?s(xmbQF&lw~@#5LBG`onT4z zNzDaZ1BMGZH)xI3Hh2ioI;(6jl%05e(|3bFYc03I;_^zCDB9jo*m|unoZFDA02BR} zGv^4w9N!7UIjvq^Wr@a~Cj`4k3B$P*pP#af!k){EelN9qlrWq_^j%2=e6IHo%6zc# zrZAl9guh%1o3FgNqs#{zZwkXHijRq#t17y+=N-Zx-wDI9R>M!F5YXI1pdKX*$5`Ia z^PyRS<{tv}C}9|Dets#7AN@v;`a3H9dxB{ZFt>#Dn8I*`KG1k(_5XtadOQTwr-Wgo zn7XE0G#)D6-euug9sX9s`b=RMA*SczGe+_JB1=?{`CJcQBTp4s@vu`IV2%zi9&wi^QlCIwth68$`uiEv? zeGGh!vZO2q-fsv)kB4IFTBu(M!yXR_T8|6`^(tX#JK`6fF~GH@A)r1b41ouyyuPJ^ z)(*Ksfqf(lEf*vE?*2N|svqkE_K+}at>V76n+1LK4$nu6K;x7!G+d41qUY)Zf5>ph zX$=ge z4fZ*EwS(25FN^nImsj>VP0~C#7<7T0U;LrF=6Z{09*jagbv&OV*H+!oN%qnH?{lic g@R)?*@!0JC0Z|%Rncwr7)Bpeg07*qoM6N<$g4dLeY5)KL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo.svg b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo.svg new file mode 100644 index 00000000000..7234e132c97 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/windfinder-logo.svg @@ -0,0 +1,3 @@ + + + From 458fa0432312c33fd866aef2000d12c7882461c4 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 14 Feb 2018 14:55:49 +0100 Subject: [PATCH 142/214] bug1301 Added new StringMessages for WindFinder "Weather Data" button --- .../home/shared/partials/windfinder/WindfinderControl.ui.xml | 4 +++- .../java/com/sap/sailing/gwt/ui/client/StringMessages.java | 2 ++ .../com/sap/sailing/gwt/ui/client/StringMessages.properties | 3 ++- .../sap/sailing/gwt/ui/client/StringMessages_de.properties | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml index f0562513370..b8c5b9a2414 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml @@ -4,7 +4,9 @@
-
Weather Data
+
+ +
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index dcad0570481..e573c16a26a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2057,4 +2057,6 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String enterIdOfWindfinderReviewedSpotCollection(); String enterTagsForTheImage(); String unableToResolveWindFinderSpotId(String id, String message); + + String windFinderWeatherData(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 1aad6db7796..a163691f71e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2044,4 +2044,5 @@ windFinder=WindFinder enterTagsForTheVideo=Enter tags for the video enterIdOfWindfinderReviewedSpotCollection=Enter ID of a reviewed WindFinder spot collection, e.g., "schilksee" enterTagsForTheImage=Enter tags for the image -unableToResolveWindFinderSpotId=Unable to resolve WindFinder spot with ID {0}: {1} \ No newline at end of file +unableToResolveWindFinderSpotId=Unable to resolve WindFinder spot with ID {0}: {1} +windFinderWeatherData=Weather Data \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 5191b0e6bac..56325852f38 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2041,4 +2041,5 @@ windFinder=WindFinder enterTagsForTheVideo=Tags zum Video erfassen enterIdOfWindfinderReviewedSpotCollection=ID einer geprüften WindFinder Messstellen-Sammlung eingeben, z.B. "schilksee" enterTagsForTheImage=Tags zum Bild erfassen -unableToResolveWindFinderSpotId=WindFinder-Station mit Kennung {0} wurde nicht gefunden: {1} \ No newline at end of file +unableToResolveWindFinderSpotId=WindFinder-Station mit Kennung {0} wurde nicht gefunden: {1} +windFinderWeatherData=Wetterdaten \ No newline at end of file From 7c897e1b59dbd86b9f6b62805f8bb0b3a1bbb8c4 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 15 Feb 2018 11:14:59 +0100 Subject: [PATCH 143/214] bug1301 First trial of WindFinder icon in RaceMap wind info window (work in progress) --- .../gwt/ui/client/shared/racemap/RaceMap.java | 27 +++++++++++++----- .../shared/racemap/RaceMapResources.java | 2 +- .../gwt/ui/client/images/windfinder.png | Bin 0 -> 6244 bytes 3 files changed, 21 insertions(+), 8 deletions(-) mode change 100755 => 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java old mode 100755 new mode 100644 index 93f3b4cfbd7..23314e43c2b --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2157,8 +2157,6 @@ public class RaceMap extends AbstractCompositeComponent impleme private Widget createInfoWindowLabelAndValue(String labelName, String value) { Label valueLabel = new Label(value); valueLabel.setWordWrap(false); - valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); return createInfoWindowLabelWithWidget(labelName, valueLabel); } @@ -2169,6 +2167,8 @@ public class RaceMap extends AbstractCompositeComponent impleme label.getElement().getStyle().setFloat(Style.Float.LEFT); label.getElement().getStyle().setPadding(3, Style.Unit.PX); label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); + value.getElement().getStyle().setFloat(Style.Float.LEFT); + value.getElement().getStyle().setPadding(3, Style.Unit.PX); flowPanel.add(label); flowPanel.add(value); return flowPanel; @@ -2225,20 +2225,33 @@ public class RaceMap extends AbstractCompositeComponent impleme positionInDecimalDegreesLabel.getElement().getStyle().setFontSize(0.7, Unit.EM); vPanel.add(positionInDecimalDegreesLabel); if (windSource.getType() == WindSourceType.WINDFINDER) { + HorizontalPanel container = new HorizontalPanel(); + container.setSpacing(2); + Image windfinderImage = new Image(raceMapImageManager.getWindFinderIconSmall()); + windfinderImage.getElement().getStyle().setMarginRight(1, Unit.EM); + container.add(windfinderImage); + container.add(vPanel); sailingService.getWindFinderSpot(windSource.getId().toString(), new AsyncCallback() { @Override public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.unableToResolveWindFinderSpotId(windSource.getId().toString(), caught.getMessage()), /* silentMode */ true); + errorReporter.reportError(stringMessages.unableToResolveWindFinderSpotId( + windSource.getId().toString(), caught.getMessage()), /* silentMode */ true); } @Override public void onSuccess(SpotDTO result) { - // TODO bug1301 place WindFinder button icon on anchor and format nicely; this or similar could be the image icon to use: RaceMap.this.raceMapImageManager.getWindFinderIconSmall(); - windSourceNameAnchor.setHref(result.getCurrentlyMostAppropriateUrl(timePoint)); - windSourceNameAnchor.setName(stringMessages.windFinder()+": "+result.getName()+" ("+result.getId()+")"); - windSourceNameAnchor.setTarget("_blank"); + // TODO bug1301 place WindFinder button icon on anchor and format nicely; this or similar could be + // the image icon to use: RaceMap.this.raceMapImageManager.getWindFinderIconSmall(); + // windSourceNameAnchor.setHref(result.getCurrentlyMostAppropriateUrl(timePoint)); + // windSourceNameAnchor.setTarget("_blank"); + windSourceNameAnchor.setText(result.getName() + " (" + result.getId() + ")"); + final String url = result.getCurrentlyMostAppropriateUrl(timePoint); + final ClickHandler clickHandler = event -> Window.open(url, "_blank", ""); + windSourceNameAnchor.addClickHandler(clickHandler); + windfinderImage.addClickHandler(clickHandler); } }); + return container; } // TODO bug1301 else, or in any case, register an event listener on the Anchor that opens the wind chart return vPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java index 7482c8385f9..62346ae44bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java @@ -28,7 +28,7 @@ public interface RaceMapResources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/maneuver_green.png") ImageResource maneuverMarkerGreen(); - @Source("com/sap/sailing/gwt/ui/client/images/windfinder_small.png") + @Source("com/sap/sailing/gwt/ui/client/images/windfinder.png") ImageResource getWindFinderIconSmall(); @Source("RaceMap.css") diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6d62a9e04f299c3923052b17fd2c42dd8cc0ad GIT binary patch literal 6244 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 O0000 Date: Thu, 15 Feb 2018 13:20:49 +0100 Subject: [PATCH 144/214] bug1301 Improved WindFinder icon in RaceMap wind info window --- .../windfinder/WindfinderControl.java | 2 + .../windfinder/WindfinderControl.ui.xml | 5 +-- .../ui/client/StringMessages_de.properties | 2 +- .../gwt/ui/client/shared/racemap/RaceMap.java | 30 +++++++------- .../shared/racemap/RaceMapImageManager.java | 5 ++- .../shared/racemap/RaceMapResources.java | 5 ++- .../client/shared/racemap/WindfinderIcon.java | 32 +++++++++++++++ .../shared/racemap/WindfinderIcon.ui.xml | 37 ++++++++++++++++++ .../gwt/ui/client/images/windfinder-logo.svg | 3 ++ .../gwt/ui/client/images/windfinder.png | Bin 6244 -> 0 bytes .../gwt/ui/client/images/windfinder_small.png | Bin 23160 -> 0 bytes 11 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml create mode 100644 java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder-logo.svg delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder_small.png diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java index 16aa04c0ddb..c6fa8c531bd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.java @@ -11,6 +11,7 @@ import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.TimePoint; public class WindfinderControl extends Widget { @@ -34,6 +35,7 @@ public class WindfinderControl extends Widget { WindfinderResources.INSTANCE.css().ensureInjected(); this.launchPadController = new WindfinderLaunchPadController(windfinderUrlFactory); setElement(uiBinder.createAndBindUi(this)); + this.labelUi.setInnerHTML(StringMessages.INSTANCE.windFinderWeatherData()); this.iconUi.setInnerHTML(local_res.windfinderLogo().getText()); sinkEvents(Event.ONCLICK); this.setVisible(false); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml index b8c5b9a2414..01fa7093dd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/windfinder/WindfinderControl.ui.xml @@ -1,12 +1,9 @@ -
-
- -
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 56325852f38..6921e51c171 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2042,4 +2042,4 @@ enterTagsForTheVideo=Tags zum Video erfassen enterIdOfWindfinderReviewedSpotCollection=ID einer geprüften WindFinder Messstellen-Sammlung eingeben, z.B. "schilksee" enterTagsForTheImage=Tags zum Bild erfassen unableToResolveWindFinderSpotId=WindFinder-Station mit Kennung {0} wurde nicht gefunden: {1} -windFinderWeatherData=Wetterdaten \ No newline at end of file +windFinderWeatherData=Wetter​daten \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 23314e43c2b..ec642cc51f2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -67,6 +67,7 @@ import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RequiresResize; @@ -885,9 +886,15 @@ public class RaceMap extends AbstractCompositeComponent impleme case EXPEDITION: case WINDFINDER: // we filter out measured wind sources with very low confidence + GWT.log("Wind source " + windSource.getId()); if (windTrackInfoDTO.minWindConfidence > 0.0001) { windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); + GWT.log("++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + } else { + GWT.log("????????????????????????????????????????????????????????"); } + if ("de15".equals(windSource.getId())) + GWT.debugger(); break; case COMBINED: showCombinedWindOnMap(windSource, windTrackInfoDTO); @@ -2217,7 +2224,7 @@ public class RaceMap extends AbstractCompositeComponent impleme final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.asDate().toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); - Label positionInDecimalDegreesLabel = new Label(" "+windDTO.position.getAsSignedDecimalDegrees()); + final Label positionInDecimalDegreesLabel = new Label(windDTO.position.getAsSignedDecimalDegrees()); positionInDecimalDegreesLabel.setWordWrap(false); positionInDecimalDegreesLabel.getElement().getStyle().setFloat(Style.Float.LEFT); positionInDecimalDegreesLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); @@ -2225,11 +2232,10 @@ public class RaceMap extends AbstractCompositeComponent impleme positionInDecimalDegreesLabel.getElement().getStyle().setFontSize(0.7, Unit.EM); vPanel.add(positionInDecimalDegreesLabel); if (windSource.getType() == WindSourceType.WINDFINDER) { - HorizontalPanel container = new HorizontalPanel(); - container.setSpacing(2); - Image windfinderImage = new Image(raceMapImageManager.getWindFinderIconSmall()); - windfinderImage.getElement().getStyle().setMarginRight(1, Unit.EM); - container.add(windfinderImage); + final HorizontalPanel container = new HorizontalPanel(); + container.setSpacing(1); + final WindfinderIcon windfinderIcon = new WindfinderIcon(raceMapImageManager, stringMessages); + container.add(windfinderIcon); container.add(vPanel); sailingService.getWindFinderSpot(windSource.getId().toString(), new AsyncCallback() { @Override @@ -2240,15 +2246,11 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void onSuccess(SpotDTO result) { - // TODO bug1301 place WindFinder button icon on anchor and format nicely; this or similar could be - // the image icon to use: RaceMap.this.raceMapImageManager.getWindFinderIconSmall(); - // windSourceNameAnchor.setHref(result.getCurrentlyMostAppropriateUrl(timePoint)); - // windSourceNameAnchor.setTarget("_blank"); - windSourceNameAnchor.setText(result.getName() + " (" + result.getId() + ")"); final String url = result.getCurrentlyMostAppropriateUrl(timePoint); - final ClickHandler clickHandler = event -> Window.open(url, "_blank", ""); - windSourceNameAnchor.addClickHandler(clickHandler); - windfinderImage.addClickHandler(clickHandler); + windSourceNameAnchor.setTarget("_blank"); + windSourceNameAnchor.setText(result.getName()); + windSourceNameAnchor.setHref(url); + windfinderIcon.setHref(url); } }); return container; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java index 280bbfc5a01..8404c590b7a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapImageManager.java @@ -12,6 +12,7 @@ import com.google.gwt.maps.client.overlays.Marker; import com.google.gwt.maps.client.overlays.MarkerImage; import com.google.gwt.maps.client.overlays.MarkerOptions; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.resources.client.TextResource; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sse.common.Util; @@ -92,7 +93,7 @@ public class RaceMapImageManager { return windSensorIconTransformer; } - public ImageResource getWindFinderIconSmall() { - return resources.getWindFinderIconSmall(); + public TextResource getWindFinderLogo() { + return resources.getWindFinderLogo(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java index 62346ae44bb..c0efb051e1f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapResources.java @@ -2,6 +2,7 @@ package com.sap.sailing.gwt.ui.client.shared.racemap; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.resources.client.TextResource; public interface RaceMapResources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/cameraBoat.png") @@ -28,8 +29,8 @@ public interface RaceMapResources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/maneuver_green.png") ImageResource maneuverMarkerGreen(); - @Source("com/sap/sailing/gwt/ui/client/images/windfinder.png") - ImageResource getWindFinderIconSmall(); + @Source("com/sap/sailing/gwt/ui/client/images/windfinder-logo.svg") + TextResource getWindFinderLogo(); @Source("RaceMap.css") RaceMapStyle raceMapStyle(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.java new file mode 100644 index 00000000000..923c678b7a0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.java @@ -0,0 +1,32 @@ +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.AnchorElement; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.StringMessages; + +public class WindfinderIcon extends Widget { + + private static WindfinderIconUiBinder uiBinder = GWT.create(WindfinderIconUiBinder.class); + + interface WindfinderIconUiBinder extends UiBinder { + } + + @UiField AnchorElement anchorUi; + @UiField DivElement iconContainerUi, textContainerUi; + + public WindfinderIcon(final RaceMapImageManager raceMapImageManager, final StringMessages stringMessages) { + setElement(uiBinder.createAndBindUi(this)); + iconContainerUi.setInnerHTML(raceMapImageManager.getWindFinderLogo().getText()); + textContainerUi.setInnerHTML(stringMessages.windFinderWeatherData()); + } + + public void setHref(String href) { + this.anchorUi.setHref(href); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml new file mode 100644 index 00000000000..33b9ecd54dd --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml @@ -0,0 +1,37 @@ + + + + .container { + display: block; + margin: .15em .5em 0 0; + } + .content { + background-color:#d0021b; + border-radius:.5em; + width: 8em; + height: 8em; + text-align: center; + color: #fff; + display: inline-block; + } + .icon { + width: 4em; + margin: .75em auto 0; + } + .icon svg { + fill: #fff; + } + .text { + font-size: 1.25em; + font-family: Arial; + padding: .33333333em .5em; + letter-spacing: 1px; + } + + +
+
+
+
+
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder-logo.svg b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder-logo.svg new file mode 100644 index 00000000000..7234e132c97 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/windfinder.png deleted file mode 100644 index 5a6d62a9e04f299c3923052b17fd2c42dd8cc0ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6244 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 Date: Thu, 15 Feb 2018 13:37:11 +0100 Subject: [PATCH 145/214] Bug 1301: trying to fix event handling in InfoWindow --- .../racemap/InfoWindowContentPanel.java | 37 +++++++++++++++++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 7 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java new file mode 100644 index 00000000000..e8fd53ad37e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java @@ -0,0 +1,37 @@ +package com.sap.sailing.gwt.ui.client.shared.racemap; + + +import java.util.Collection; +import java.util.HashSet; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.maps.client.overlays.InfoWindow; +import com.google.gwt.user.client.ui.SimplePanel; + +public class InfoWindowContentPanel extends SimplePanel { + private boolean attached = false; + + public InfoWindowContentPanel(InfoWindow infoWindowToSetTo) { + final Collection handlerRegistrations = new HashSet<>(); + handlerRegistrations.add(infoWindowToSetTo.addDomReadyHandler(event -> maybeAttach())); + handlerRegistrations.add(infoWindowToSetTo.addCloseClickHandler(event -> maybeDetach())); + handlerRegistrations.add(infoWindowToSetTo.addContentChangeHandler(event -> { + handlerRegistrations.forEach(HandlerRegistration::removeHandler); + maybeDetach(); + })); + } + + private void maybeAttach() { + if (!attached) { + attached = true; + onAttach(); + } + } + + private void maybeDetach() { + if (attached) { + attached = false; + onDetach(); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index ec642cc51f2..9d590408eba 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2152,9 +2152,10 @@ public class RaceMap extends AbstractCompositeComponent impleme lastInfoWindow.close(); } LatLng where = coordinateSystem.toLatLng(windDTO.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(windSource, windTrackInfoDTO)); + final InfoWindowOptions options = InfoWindowOptions.newInstance(); + final InfoWindow infoWindow = InfoWindow.newInstance(options); + final InfoWindowContentPanel contentPanel = new InfoWindowContentPanel(infoWindow); + contentPanel.setWidget(getInfoWindowContent(windSource, windTrackInfoDTO)); infoWindow.setPosition(where); lastInfoWindow = infoWindow; infoWindow.open(map); From d8d1a52476a118667dc20aaaa23d9432a1ef58b4 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 15 Feb 2018 14:29:48 +0100 Subject: [PATCH 146/214] bug1301 Removed no longer needed log outputs and simplified markup --- .../racemap/InfoWindowContentPanel.java | 4 ++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 6 ------ .../shared/racemap/WindfinderIcon.ui.xml | 19 ++++++++----------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java index e8fd53ad37e..377e83afcab 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java @@ -4,6 +4,7 @@ package com.sap.sailing.gwt.ui.client.shared.racemap; import java.util.Collection; import java.util.HashSet; +import com.google.gwt.core.shared.GWT; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.maps.client.overlays.InfoWindow; import com.google.gwt.user.client.ui.SimplePanel; @@ -12,6 +13,7 @@ public class InfoWindowContentPanel extends SimplePanel { private boolean attached = false; public InfoWindowContentPanel(InfoWindow infoWindowToSetTo) { + infoWindowToSetTo.setContent(this); final Collection handlerRegistrations = new HashSet<>(); handlerRegistrations.add(infoWindowToSetTo.addDomReadyHandler(event -> maybeAttach())); handlerRegistrations.add(infoWindowToSetTo.addCloseClickHandler(event -> maybeDetach())); @@ -25,6 +27,7 @@ public class InfoWindowContentPanel extends SimplePanel { if (!attached) { attached = true; onAttach(); + GWT.log("Attaching info window"); } } @@ -32,6 +35,7 @@ public class InfoWindowContentPanel extends SimplePanel { if (attached) { attached = false; onDetach(); + GWT.log("Detaching info window"); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 9d590408eba..272562c1daf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -886,15 +886,9 @@ public class RaceMap extends AbstractCompositeComponent impleme case EXPEDITION: case WINDFINDER: // we filter out measured wind sources with very low confidence - GWT.log("Wind source " + windSource.getId()); if (windTrackInfoDTO.minWindConfidence > 0.0001) { windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); - GWT.log("++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); - } else { - GWT.log("????????????????????????????????????????????????????????"); } - if ("de15".equals(windSource.getId())) - GWT.debugger(); break; case COMBINED: showCombinedWindOnMap(windSource, windTrackInfoDTO); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml index 33b9ecd54dd..1c6634b10a4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/WindfinderIcon.ui.xml @@ -2,20 +2,19 @@ .container { - display: block; - margin: .15em .5em 0 0; - } - .content { - background-color:#d0021b; - border-radius:.5em; + display: inline-block; width: 8em; height: 8em; + margin: .15em .5em 0 0; text-align: center; + text-decoration: none; color: #fff; - display: inline-block; + background-color:#d0021b; + border-radius:.5em; } .icon { width: 4em; + height: auto; margin: .75em auto 0; } .icon svg { @@ -29,9 +28,7 @@ } -
-
-
-
+
+
\ No newline at end of file From baa0209b81ca7b3d96752637c61c5bc5e27c156b Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 15 Feb 2018 14:53:28 +0100 Subject: [PATCH 147/214] bug1301 Open wind chart from RaceMap wind info window for other wind sources --- .../gwt/ui/client/shared/racemap/RaceMap.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 272562c1daf..dd863d88d19 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; @@ -110,6 +111,7 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.Competitor import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.raceboard.RaceBoardPanel; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; @@ -430,6 +432,9 @@ public class RaceMap extends AbstractCompositeComponent impleme protected Label estimatedDurationOverlay; private RaceMapStyle raceMapStyle; private final boolean showHeaderPanel; + + /** Callback to set the visibility of the wind chart. */ + private final Consumer setWindChartVisibleCallback; private class AdvantageLineUpdater implements QuickRanksListener { @Override @@ -442,7 +447,7 @@ public class RaceMap extends AbstractCompositeComponent impleme } } - public RaceMap(Component parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, + public RaceMap(RaceBoardPanel parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, RaceMapSettings raceMapSettings, SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, RaceCompetitorSet raceCompetitorSet, @@ -459,6 +464,8 @@ public class RaceMap extends AbstractCompositeComponent impleme this.errorReporter = errorReporter; this.timer = timer; this.isSimulationEnabled = true; + this.setWindChartVisibleCallback = parent == null ? visibile -> { + } : parent::setWindChartVisible; timer.addTimeListener(this); raceMapImageManager = new RaceMapImageManager(raceMapResources); markDTOs = new HashMap(); @@ -2249,7 +2256,9 @@ public class RaceMap extends AbstractCompositeComponent impleme } }); return container; - } // TODO bug1301 else, or in any case, register an event listener on the Anchor that opens the wind chart + } else { + windSourceNameAnchor.addClickHandler(event -> setWindChartVisibleCallback.accept(true)); + } return vPanel; } From f5a7e8d89379c2edfc6cf8429354b6639eccb2a3 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 15 Feb 2018 16:55:14 +0100 Subject: [PATCH 148/214] bug1301 Improved RaceMap info window management / remove no longer used code --- .../racemap/InfoWindowContentPanel.java | 41 -------- .../shared/racemap/ManagedInfoWindow.java | 67 +++++++++++++ .../ManeuverMarkerClickedListener.java | 34 ------- .../gwt/ui/client/shared/racemap/RaceMap.java | 99 ++++++------------- 4 files changed, 96 insertions(+), 145 deletions(-) delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkerClickedListener.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java deleted file mode 100644 index 377e83afcab..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/InfoWindowContentPanel.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sap.sailing.gwt.ui.client.shared.racemap; - - -import java.util.Collection; -import java.util.HashSet; - -import com.google.gwt.core.shared.GWT; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.maps.client.overlays.InfoWindow; -import com.google.gwt.user.client.ui.SimplePanel; - -public class InfoWindowContentPanel extends SimplePanel { - private boolean attached = false; - - public InfoWindowContentPanel(InfoWindow infoWindowToSetTo) { - infoWindowToSetTo.setContent(this); - final Collection handlerRegistrations = new HashSet<>(); - handlerRegistrations.add(infoWindowToSetTo.addDomReadyHandler(event -> maybeAttach())); - handlerRegistrations.add(infoWindowToSetTo.addCloseClickHandler(event -> maybeDetach())); - handlerRegistrations.add(infoWindowToSetTo.addContentChangeHandler(event -> { - handlerRegistrations.forEach(HandlerRegistration::removeHandler); - maybeDetach(); - })); - } - - private void maybeAttach() { - if (!attached) { - attached = true; - onAttach(); - GWT.log("Attaching info window"); - } - } - - private void maybeDetach() { - if (attached) { - attached = false; - onDetach(); - GWT.log("Detaching info window"); - } - } -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java new file mode 100644 index 00000000000..c172ff2f005 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java @@ -0,0 +1,67 @@ +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.maps.client.MapWidget; +import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.maps.client.overlays.InfoWindow; +import com.google.gwt.maps.client.overlays.InfoWindowOptions; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; + +public class ManagedInfoWindow { + + private final InfoWindowOptions infoWindowOptions = InfoWindowOptions.newInstance(); + private final InfoWindow infoWindow = InfoWindow.newInstance(infoWindowOptions); + private final MapWidget map; + + private final FooBarBlubbKeineAhnung container = new FooBarBlubbKeineAhnung(); + + private boolean shown = false; + + public ManagedInfoWindow(final MapWidget map) { + this.map = map; + this.infoWindow.setContent(container); + this.infoWindow.addDomReadyHandler(event -> container.maybeAttach()); + this.infoWindow.addCloseClickHandler(event -> { + container.maybeDetach(); + shown = false; + }); + } + + public void openAtPosition(final Widget content, final LatLng position) { + this.container.setWidget(content); + this.infoWindow.setPosition(position); + this.infoWindow.open(map); + this.shown = true; + } + + public void close() { + if (shown) { + this.container.maybeDetach(); + this.infoWindow.close(); + this.shown = false; + } + } + + private class FooBarBlubbKeineAhnung extends SimplePanel { + + private boolean attached = false; + + private void maybeAttach() { + if (!attached) { + this.attached = true; + onAttach(); + GWT.log("++++++++++++ Attaching info window ++++++++++++"); + } + } + + private void maybeDetach() { + if (attached) { + this.attached = false; + onDetach(); + GWT.log("------------ Detaching info window ------------"); + } + } + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkerClickedListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkerClickedListener.java deleted file mode 100644 index df78d130fff..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkerClickedListener.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sap.sailing.gwt.ui.client.shared.racemap; - -import com.google.gwt.maps.client.base.LatLng; -import com.google.gwt.maps.client.events.click.ClickMapEvent; -import com.google.gwt.maps.client.events.click.ClickMapHandler; -import com.google.gwt.maps.client.overlays.InfoWindow; -import com.google.gwt.maps.client.overlays.InfoWindowOptions; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; - -public class ManeuverMarkerClickedListener implements ClickMapHandler { - - private RaceMap racemap; - private ManeuverDTO maneuver; - - public ManeuverMarkerClickedListener(RaceMap racemap, ManeuverDTO maneuver) { - this.racemap = racemap; - this.maneuver = maneuver; - } - - @Override - public void onEvent(ClickMapEvent event) { - final InfoWindow lastInfoWindow = racemap.getLastInfoWindow(); - if (lastInfoWindow != null) { - lastInfoWindow.close(); - } - LatLng where = racemap.getCoordinateSystem().toLatLng(maneuver.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(racemap.getInfoWindowContent(maneuver)); - infoWindow.setPosition(where); - racemap.setLastInfoWindow(infoWindow); - infoWindow.open(racemap.getMap()); - } -} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index dd863d88d19..24542a9929c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -53,8 +53,6 @@ import com.google.gwt.maps.client.events.zoom.ZoomChangeMapEvent; import com.google.gwt.maps.client.events.zoom.ZoomChangeMapHandler; import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType; import com.google.gwt.maps.client.mvc.MVCArray; -import com.google.gwt.maps.client.overlays.InfoWindow; -import com.google.gwt.maps.client.overlays.InfoWindowOptions; import com.google.gwt.maps.client.overlays.Marker; import com.google.gwt.maps.client.overlays.MarkerOptions; import com.google.gwt.maps.client.overlays.Polygon; @@ -331,8 +329,6 @@ public class RaceMap extends AbstractCompositeComponent impleme private RaceTimesInfoDTO lastRaceTimesInfo; - private InfoWindow lastInfoWindow = null; - /** * RPC calls may receive responses out of order if there are multiple calls in-flight at the same time. If the time * slider is moved quickly it generates many requests for boat positions quickly after each other. Sometimes, @@ -435,6 +431,7 @@ public class RaceMap extends AbstractCompositeComponent impleme /** Callback to set the visibility of the wind chart. */ private final Consumer setWindChartVisibleCallback; + private ManagedInfoWindow managedInfoWindow; private class AdvantageLineUpdater implements QuickRanksListener { @Override @@ -464,8 +461,7 @@ public class RaceMap extends AbstractCompositeComponent impleme this.errorReporter = errorReporter; this.timer = timer; this.isSimulationEnabled = true; - this.setWindChartVisibleCallback = parent == null ? visibile -> { - } : parent::setWindChartVisible; + this.setWindChartVisibleCallback = parent == null ? visibile -> {} : parent::setWindChartVisible; timer.addTimeListener(this); raceMapImageManager = new RaceMapImageManager(raceMapResources); markDTOs = new HashMap(); @@ -709,6 +705,7 @@ public class RaceMap extends AbstractCompositeComponent impleme RaceMap.this.redraw(); trueNorthIndicatorPanel.redraw(); showAdditionalControls(map); + RaceMap.this.managedInfoWindow = new ManagedInfoWindow(map); } }; LoadApi.go(onLoad, loadLibraries, sensor, "key="+GoogleMapAPIKey.V3_APIKey); @@ -2073,21 +2070,11 @@ public class RaceMap extends AbstractCompositeComponent impleme private BoatOverlay createBoatOverlay(int zIndex, final CompetitorDTO competitorDTO, DisplayMode displayMode) { final BoatOverlay boatCanvas = new BoatOverlay(map, zIndex, competitorDTO, competitorSelection.getColor(competitorDTO, raceIdentifier), coordinateSystem); boatCanvas.setDisplayMode(displayMode); - boatCanvas.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - if (lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } + boatCanvas.addClickHandler(event -> { + GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + Widget content = getInfoWindowContent(competitorDTO, latestFixForCompetitor); + LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); + managedInfoWindow.openAtPosition(content, where); }); boatCanvas.addMouseOverHandler(new MouseOverMapHandler() { @@ -2119,29 +2106,13 @@ public class RaceMap extends AbstractCompositeComponent impleme } private void showMarkInfoWindow(MarkDTO markDTO, LatLng position) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(markDTO)); - infoWindow.setPosition(position); - lastInfoWindow = infoWindow; - infoWindow.open(map); + managedInfoWindow.openAtPosition(getInfoWindowContent(markDTO), position); } private void showCompetitorInfoWindow(final CompetitorDTO competitorDTO, LatLng where) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - // TODO find close fix where the mouse was; see BUG 470 - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); + final GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + final Widget content = getInfoWindowContent(competitorDTO, latestFixForCompetitor); + managedInfoWindow.openAtPosition(content, where); } private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { @@ -2149,17 +2120,9 @@ public class RaceMap extends AbstractCompositeComponent impleme WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); if (windDTO != null && windDTO.position != null) { - if (lastInfoWindow != null) { - lastInfoWindow.close(); - } - LatLng where = coordinateSystem.toLatLng(windDTO.position); - final InfoWindowOptions options = InfoWindowOptions.newInstance(); - final InfoWindow infoWindow = InfoWindow.newInstance(options); - final InfoWindowContentPanel contentPanel = new InfoWindowContentPanel(infoWindow); - contentPanel.setWidget(getInfoWindowContent(windSource, windTrackInfoDTO)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); + final LatLng where = coordinateSystem.toLatLng(windDTO.position); + final Widget content = getInfoWindowContent(windSource, windTrackInfoDTO); + managedInfoWindow.openAtPosition(content, where); } } @@ -2191,7 +2154,7 @@ public class RaceMap extends AbstractCompositeComponent impleme return vPanel; } - public Widget getInfoWindowContent(ManeuverDTO maneuver) { + private Widget getInfoWindowContent(ManeuverDTO maneuver) { SpeedWithBearingDTO before = maneuver.speedWithBearingBefore; SpeedWithBearingDTO after = maneuver.speedWithBearingAfter; VerticalPanel vPanel = new VerticalPanel(); @@ -2418,18 +2381,22 @@ public class RaceMap extends AbstractCompositeComponent impleme } private void createAndAddMarkerOfManeuver(ManeuverDTO maneuver) { - LatLng latLng = coordinateSystem.toLatLng(maneuver.position); + LatLng latLng = coordinateSystem.toLatLng(maneuver.position); Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndDirectionIndicatingColor .get(new Pair(maneuver.type, ManeuverColor.getManeuverColor(maneuver))); - MarkerOptions options = MarkerOptions.newInstance(); - options.setIcon(maneuverMarker.getIcon_MarkerImage()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); + MarkerOptions options = MarkerOptions.newInstance(); + options.setIcon(maneuverMarker.getIcon_MarkerImage()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); marker.setTitle(ManeuverTypeFormatter.format(maneuver.type, stringMessages)); - marker.addClickHandler(new ManeuverMarkerClickedListener(this, maneuver)); - maneuverMarkers.add(marker); - marker.setMap(map); - } + marker.addClickHandler(event -> { + LatLng where = getCoordinateSystem().toLatLng(maneuver.position); + Widget content = getInfoWindowContent(maneuver); + managedInfoWindow.openAtPosition(content, where); + }); + maneuverMarkers.add(marker); + marker.setMap(map); + } /** * @param date @@ -3055,14 +3022,6 @@ public class RaceMap extends AbstractCompositeComponent impleme } } - void setLastInfoWindow(InfoWindow infoWindow) { - lastInfoWindow = infoWindow; - } - - InfoWindow getLastInfoWindow() { - return lastInfoWindow; - } - public void addCompetitorsForRaceDefinedListener(CompetitorsForRaceDefinedListener listener) { raceCompetitorSet.addCompetitorsForRaceDefinedListener(listener); } From 46da046b7f0d687c490185086877c9f5c044b0a6 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 15 Feb 2018 17:16:14 +0100 Subject: [PATCH 149/214] bug1301 Improved i18n for sailor info text in French --- .../com/sap/sailing/gwt/ui/client/StringMessages_fr.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index 4ea3990fe20..0cc6dcefc8d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -1411,7 +1411,7 @@ calculatedTimeTraveledTooltip=Temps calculé selon le handicap du concurrent \nb raceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds=Temps calculé @ par rapport au plus rapide raceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSecondsTooltip=Temps calculé selon l''arrivée estimée du concurrent qui est actuellement en tête de la course ; \nestimation basée sur la VMG dans la portion de parcours actuelle, équivaut à la performance (calculée) du bateau le plus rapide dans toutes les portions de parcours suivantes. sailorInfo=Informations concurrents -sailorInfoLongText=Résultats officiels, réclamations, météo marine +sailorInfoLongText=Résultats officiels,\nréclamations, météo marine: eventQuickfinder=Quickfinder manifestation regattaQuickfinder=Quickfinder régate resultsQuickfinder=Quickfinder résultats From ad3b1067496878b4fe368926b5d87415dc2afdc2 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Thu, 15 Feb 2018 17:25:06 +0100 Subject: [PATCH 150/214] ensure the zoom buttons are properly removed, if the range is reset --- .../ui/client/shared/charts/AbstractRaceChart.java | 1 + .../moxieapps/gwt/highcharts/client/BaseChart.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index d57444571d8..e8697573574 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -245,6 +245,7 @@ public abstract class AbstractRaceChart extends A protected void resetMinMaxAndExtremesInterval(boolean redraw) { changeMinMaxAndExtremesInterval(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), redraw); + chart.hideResetZoom(); } @Override diff --git a/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/BaseChart.java b/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/BaseChart.java index 5ad43857455..0f0dbdeb71a 100755 --- a/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/BaseChart.java +++ b/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/BaseChart.java @@ -3046,4 +3046,17 @@ public abstract class BaseChart extends Widget { if (chart) chart.showResetZoom(); }-*/; + + /** + * Patch, to provide highcharts functionality in Java API. + */ + public native void hideResetZoom() /*-{ + var chart = this.@org.moxieapps.gwt.highcharts.client.BaseChart::chart; + if (chart) { + if (chart.resetZoomButton) { + chart.zoomOut(); + } + } + }-*/; + } From 13df3fe96908b4200a94201d69824aa8b812e22a Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Fri, 16 Feb 2018 11:18:26 +0100 Subject: [PATCH 151/214] fixed readObject to work equivalent for windFinderReviewedSpotsCollectionIds as it does for leaderboardGroups, this also appears to allow to import data from a non windFinder server --- .../src/com/sap/sailing/domain/base/impl/EventImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java index 4ba51806e87..ceb732d4e0b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/EventImpl.java @@ -25,7 +25,7 @@ public class EventImpl extends EventBaseImpl implements Event { private ConcurrentLinkedQueue leaderboardGroups; - private final ConcurrentMap windFinderReviewedSpotsCollectionIds; + private ConcurrentMap windFinderReviewedSpotsCollectionIds; public EventImpl(String name, TimePoint startDate, TimePoint endDate, String venueName, boolean isPublic, UUID id) { this(name, startDate, endDate, new VenueImpl(venueName), isPublic, id); @@ -45,6 +45,9 @@ public class EventImpl extends EventBaseImpl implements Event { if (leaderboardGroups == null) { leaderboardGroups = new ConcurrentLinkedQueue<>(); } + if (windFinderReviewedSpotsCollectionIds == null) { + windFinderReviewedSpotsCollectionIds = new ConcurrentHashMap<>(); + } } public String toString() { From 8346b5929fa8c142f86d3b4a2b6674775690a9f1 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Fri, 16 Feb 2018 15:39:31 +0100 Subject: [PATCH 152/214] fixed issue with non compiling due to invalid class usages --- .../gwt/ui/client/shared/racemap/RaceMap.java | 16 +++++++++++++--- .../sailing/gwt/ui/raceboard/RaceBoardPanel.java | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 24542a9929c..bfc82b3754d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -109,7 +109,6 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.Competitor import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -import com.sap.sailing.gwt.ui.raceboard.RaceBoardPanel; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; @@ -444,12 +443,23 @@ public class RaceMap extends AbstractCompositeComponent impleme } } - public RaceMap(RaceBoardPanel parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, + public RaceMap(Component parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, RaceMapSettings raceMapSettings, SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, RaceCompetitorSet raceCompetitorSet, StringMessages stringMessages, RegattaAndRaceIdentifier raceIdentifier, RaceMapResources raceMapResources, boolean showHeaderPanel, QuickRanksDTOProvider quickRanksDTOProvider) { + this(parent, context, raceMapLifecycle, raceMapSettings, sailingService, asyncActionsExecutor, errorReporter, + timer, competitorSelection, raceCompetitorSet, stringMessages, raceIdentifier, raceMapResources, + showHeaderPanel, quickRanksDTOProvider, visible -> {}); + } + + public RaceMap(Component parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, + RaceMapSettings raceMapSettings, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, RaceCompetitorSet raceCompetitorSet, + StringMessages stringMessages, RegattaAndRaceIdentifier raceIdentifier, + RaceMapResources raceMapResources, boolean showHeaderPanel, QuickRanksDTOProvider quickRanksDTOProvider, Consumer setWindChartVisibleCallback) { super(parent, context); this.showHeaderPanel = showHeaderPanel; this.quickRanksDTOProvider = quickRanksDTOProvider; @@ -461,7 +471,7 @@ public class RaceMap extends AbstractCompositeComponent impleme this.errorReporter = errorReporter; this.timer = timer; this.isSimulationEnabled = true; - this.setWindChartVisibleCallback = parent == null ? visibile -> {} : parent::setWindChartVisible; + this.setWindChartVisibleCallback = setWindChartVisibleCallback; timer.addTimeListener(this); raceMapImageManager = new RaceMapImageManager(raceMapResources); markDTOs = new HashMap(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 9199ee1ea03..2e6553bbbd7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -223,7 +223,7 @@ public class RaceBoardPanel raceMap = new RaceMap(this, componentContext, raceMapLifecycle, defaultRaceMapSettings, sailingService, asyncActionsExecutor, errorReporter, timer, competitorSelectionProvider, raceCompetitorSet, stringMessages, selectedRaceIdentifier, - raceMapResources, /* showHeaderPanel */ true, quickRanksDTOProvider) { + raceMapResources, /* showHeaderPanel */ true, quickRanksDTOProvider, this::setWindChartVisible) { private static final String INDENT_SMALL_CONTROL_STYLE = "indentsmall"; private static final String INDENT_BIG_CONTROL_STYLE = "indentbig"; @Override From ceeee64a36d1f7b37e38b7011e36701628fc02b4 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 19 Feb 2018 14:40:50 +0100 Subject: [PATCH 153/214] added i18n for TrackedRaces/TrackedRaces timing panel Change-Id: Ifcd81856f5f24b721233ae987d7b6893a23c902b --- .../ui/adminconsole/TrackedRacesManagementPanel.java | 12 ++++++------ .../sap/sailing/gwt/ui/client/StringMessages.java | 1 + .../sailing/gwt/ui/client/StringMessages.properties | 1 + .../gwt/ui/client/StringMessages_de.properties | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesManagementPanel.java index 7ac369d07fd..dc42407db41 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesManagementPanel.java @@ -66,12 +66,12 @@ public class TrackedRacesManagementPanel extends AbstractRaceManagementPanel { raceDataGrid = new Grid(6,2); this.selectedRaceContentPanel.add(raceDataGrid); - raceDataGrid.setText(0, 0, "StartTime:"); - raceDataGrid.setText(1, 0, "EndTime:"); - raceDataGrid.setText(2, 0, "Duration:"); - raceDataGrid.setText(3, 0, "Start of tracking:"); - raceDataGrid.setText(4, 0, "End of tracking:"); - raceDataGrid.setText(5, 0, "Delay to live (ms):"); + raceDataGrid.setText(0, 0, stringMessages.startTime()+":"); + raceDataGrid.setText(1, 0, stringMessages.endTime()+":"); + raceDataGrid.setText(2, 0, stringMessages.duration()+":"); + raceDataGrid.setText(3, 0, stringMessages.startOfTracking()+":"); + raceDataGrid.setText(4, 0, stringMessages.endOfTracking()+":"); + raceDataGrid.setText(5, 0, stringMessages.delayForLiveMode()+" (ms):"); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 097d301b161..bafd66913ee 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -110,6 +110,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String eventName(); String event(); String startTime(); + String endTime(); String startOfTracking(); String endOfTracking(); String regatta(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index c009fff343d..2d76a1f8fb8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -108,6 +108,7 @@ regattaName=Regatta name eventName=Event name event=Event startTime=Start time +endTime=End time startOfTracking=Start of tracking endOfTracking=End of tracking regatta=Regatta diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 99d8c6518b4..dedd3a31108 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -109,6 +109,7 @@ regattaName=Name der Regatta eventName=Event-Name event=Veranstaltung startTime=Startzeit +endTime=Schlusszeit startOfTracking=Start des Trackings endOfTracking=Ende des Trackings regatta=Regatta From 85572cc372318935eb965bfb5f0e1c86a7b70539 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 19 Feb 2018 15:25:13 +0100 Subject: [PATCH 154/214] Bug 4380: Copying more RaceLogEvents when slicing --- .../gwt/ui/server/SailingServiceImpl.java | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index dde94e62a72..366ff8b21f3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -69,8 +69,14 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningListChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogPathfinderEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; @@ -88,14 +94,21 @@ import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConfirmedEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningListChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFixedMarkPassingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFlagEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogGateLineOpeningTimeEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogPathfinderEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogProtestStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogSuppressedMarkPassingsEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl; +import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; +import com.sap.sailing.domain.abstractlog.race.scoring.impl.RaceLogAdditionalScoringInformationEventImpl; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.FlagPoleState; @@ -7070,6 +7083,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final StartTimeFinderResult startTimeFinderResult = new StartTimeFinder(getService(), raceLogOfRaceToSlice).analyze(); final TimePoint startTime = startTimeFinderResult.getStartTime(); final boolean hasStartTime = startTime != null && timeRange.includes(startTime); + boolean hasFinishedTime = false; if (hasStartTime) { // we do not support depdendent start times here raceLog.add(new RaceLogStartTimeEventImpl(startTimeFinderResult.getStartTime(), author, @@ -7080,10 +7094,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final TimePoint finishedTime = new FinishedTimeFinder(raceLog).analyze(); if (finishedTime != null && timeRange.includes(finishedTime)) { raceLog.add(new RaceLogRaceStatusEventImpl(finishedTime, author, raceLog.getCurrentPassId(), RaceLogRaceStatus.FINISHED)); + hasFinishedTime = true; } } } - + final boolean hasFinishedTimeFinal = hasFinishedTime; // Only wind fixes in the new tracking interval as well as the best fallback fixes are added to the new RaceLog final LogEventTimeRangeWithFallbackFilter windFixEvents = new LogEventTimeRangeWithFallbackFilter<>( @@ -7138,10 +7153,91 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } + @Override + public void visit(RaceLogFinishPositioningConfirmedEvent event) { + if (hasFinishedTimeFinal && isLatestPass(event)) { + raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + } + } + + @Override + public void visit(RaceLogFinishPositioningListChangedEvent event) { + if (hasFinishedTimeFinal && isLatestPass(event)) { + raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + } + } + + @Override + public void visit(RaceLogFixedMarkPassingEvent event) { + if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getTimePointOfFixedPassing())) { + raceLog.add(new RaceLogFixedMarkPassingEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + event.getInvolvedBoats(), raceLog.getCurrentPassId(), + event.getTimePointOfFixedPassing(), event.getZeroBasedIndexOfPassedWaypoint())); + } + } + + @Override + public void visit(RaceLogSuppressedMarkPassingsEvent event) { + if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getLogicalTimePoint())) { + raceLog.add(new RaceLogSuppressedMarkPassingsEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + event.getInvolvedBoats(), raceLog.getCurrentPassId(), + event.getZeroBasedIndexOfFirstSuppressedWaypoint())); + } + } + + @Override + public void visit(RaceLogProtestStartTimeEvent event) { + if (hasFinishedTimeFinal && isLatestPass(event)) { + raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getProtestTime())); + } + } + + @Override + public void visit(RaceLogAdditionalScoringInformationEvent event) { + if (hasFinishedTimeFinal && isLatestPass(event)) { + raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.getCurrentPassId(), event.getType())); + } + } + + @Override + public void visit(RaceLogPathfinderEvent event) { + if (hasStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogPathfinderEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), + event.getPathfinderId())); + } + } + + @Override + public void visit(RaceLogGateLineOpeningTimeEvent event) { + if (hasStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogGateLineOpeningTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getGateLineOpeningTimes().getGateLaunchStopTime(), + event.getGateLineOpeningTimes().getGolfDownTime())); + } + } + + @Override + public void visit(RaceLogRaceStatusEvent event) { + if (isLatestPass(event)) { + if ((hasStartTime && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber())) { + new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), + event.getNextStatus()); + } + } + } + private boolean isLatestPass(RaceLogEvent event) { return event.getPassId() == raceLogOfRaceToSlice.getCurrentPassId(); } - // TODO do we need to copy more events? }); } From e11c85b0e6cb2572691d3d52c770c4dbc6ff97d9 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 19 Feb 2018 16:07:36 +0100 Subject: [PATCH 155/214] Bug 4380: Implemented a more generic way to copy race state changes to a sliced race and implemented partial support for copying dependent start times --- .../gwt/ui/server/SailingServiceImpl.java | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 366ff8b21f3..266060e2675 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -67,6 +67,7 @@ import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; @@ -79,6 +80,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogProtestStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.AbortingFlagFinder; @@ -93,6 +95,7 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesFinde import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogDependentStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConfirmedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningListChangedEventImpl; @@ -7083,28 +7086,47 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final StartTimeFinderResult startTimeFinderResult = new StartTimeFinder(getService(), raceLogOfRaceToSlice).analyze(); final TimePoint startTime = startTimeFinderResult.getStartTime(); final boolean hasStartTime = startTime != null && timeRange.includes(startTime); - boolean hasFinishedTime = false; + final boolean dependentStartTime = startTimeFinderResult.isDependentStartTime(); + final boolean hasFinishingTime; + final boolean hasFinishedTime; if (hasStartTime) { - // we do not support depdendent start times here - raceLog.add(new RaceLogStartTimeEventImpl(startTimeFinderResult.getStartTime(), author, - raceLog.getCurrentPassId(), startTimeFinderResult.getStartTime())); final TimePoint finishingTime = new FinishingTimeFinder(raceLog).analyze(); - if (finishingTime != null && timeRange.includes(finishingTime)) { - raceLog.add(new RaceLogRaceStatusEventImpl(finishingTime, author, raceLog.getCurrentPassId(), RaceLogRaceStatus.FINISHING)); + hasFinishingTime = finishingTime != null && timeRange.includes(finishingTime); + if (hasFinishingTime) { final TimePoint finishedTime = new FinishedTimeFinder(raceLog).analyze(); - if (finishedTime != null && timeRange.includes(finishedTime)) { - raceLog.add(new RaceLogRaceStatusEventImpl(finishedTime, author, raceLog.getCurrentPassId(), RaceLogRaceStatus.FINISHED)); - hasFinishedTime = true; - } + hasFinishedTime = finishedTime != null && timeRange.includes(finishedTime); + } else { + hasFinishedTime = false; } + } else { + hasFinishingTime = false; + hasFinishedTime = false; } - final boolean hasFinishedTimeFinal = hasFinishedTime; // Only wind fixes in the new tracking interval as well as the best fallback fixes are added to the new RaceLog final LogEventTimeRangeWithFallbackFilter windFixEvents = new LogEventTimeRangeWithFallbackFilter<>( timeRange); for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { raceLogEvent.accept(new BaseRaceLogEventVisitor() { + @Override + public void visit(RaceLogDependentStartTimeEvent event) { + if (dependentStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogDependentStartTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getDependentOnRaceIdentifier(), + event.getStartTimeDifference(), event.getNextStatus())); + } + } + + @Override + public void visit(RaceLogStartTimeEvent event) { + if (!dependentStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogStartTimeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getStartTime(), + event.getNextStatus())); + } + } + @Override public void visit(RaceLogRegisterCompetitorEvent event) { raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getLogicalTimePoint(), event.getAuthor(), @@ -7131,7 +7153,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogFlagEvent event) { - // If we do not have a start time in the new race, flag events won't be useful if (hasStartTime && isLatestPass(event) && !event.getLogicalTimePoint().after(sliceTo)) { raceLog.add(new RaceLogFlagEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getUpperFlag(), event.getLowerFlag(), @@ -7155,7 +7176,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogFinishPositioningConfirmedEvent event) { - if (hasFinishedTimeFinal && isLatestPass(event)) { + if (hasFinishedTime && isLatestPass(event)) { raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); } @@ -7163,7 +7184,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogFinishPositioningListChangedEvent event) { - if (hasFinishedTimeFinal && isLatestPass(event)) { + if (hasFinishedTime && isLatestPass(event)) { raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); } @@ -7191,7 +7212,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogProtestStartTimeEvent event) { - if (hasFinishedTimeFinal && isLatestPass(event)) { + if (hasFinishedTime && isLatestPass(event)) { raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getProtestTime())); } @@ -7199,7 +7220,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogAdditionalScoringInformationEvent event) { - if (hasFinishedTimeFinal && isLatestPass(event)) { + if (hasFinishedTime && isLatestPass(event)) { raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getLogicalTimePoint(), event.getAuthor(), raceLog.getCurrentPassId(), event.getType())); } @@ -7226,8 +7247,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogRaceStatusEvent event) { - if (isLatestPass(event)) { - if ((hasStartTime && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber())) { + if (isLatestPass(event) && !(event instanceof RaceLogDependentStartTimeEvent) + && !(event instanceof RaceLogStartTimeEvent)) { + if ((hasStartTime + && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber()) + || (hasFinishingTime && event.getNextStatus() == RaceLogRaceStatus.FINISHING) + || (hasFinishedTime && event.getNextStatus() == RaceLogRaceStatus.FINISHED)) { new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getNextStatus()); From eead825f829a2e2d44ebea1ef15a5b364c36cd04 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 19 Feb 2018 16:15:05 +0100 Subject: [PATCH 156/214] Bug 4380: Adjustments to retain most data for RaceLogEvents of sliced races --- .../gwt/ui/server/SailingServiceImpl.java | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 266060e2675..1b0d1d2dc51 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7039,7 +7039,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return new SliceRacePreperationDTO(prefix + currentCount, alreadyUsedRaceNames); } - @Override public RegattaAndRaceIdentifier sliceRace(RegattaAndRaceIdentifier raceIdentifier, String newRaceColumnName, TimePoint sliceFrom, TimePoint sliceTo) { @@ -7129,7 +7128,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogRegisterCompetitorEvent event) { - raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getCompetitor())); } @@ -7146,30 +7146,31 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogCourseDesignChangedEvent event) { - raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getLogicalTimePoint(), - event.getAuthor(), raceLog.getCurrentPassId(), event.getCourseDesign(), - event.getCourseDesignerMode())); + raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getCourseDesign(), event.getCourseDesignerMode())); } @Override public void visit(RaceLogFlagEvent event) { if (hasStartTime && isLatestPass(event) && !event.getLogicalTimePoint().after(sliceTo)) { - raceLog.add(new RaceLogFlagEventImpl(event.getLogicalTimePoint(), event.getAuthor(), - raceLog.getCurrentPassId(), event.getUpperFlag(), event.getLowerFlag(), - event.isDisplayed())); + raceLog.add(new RaceLogFlagEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getUpperFlag(), + event.getLowerFlag(), event.isDisplayed())); } } @Override public void visit(RaceLogCourseAreaChangedEvent event) { - raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), - raceLog.getCurrentPassId(), event.getCourseAreaId())); + raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getCourseAreaId())); } @Override public void visit(RaceLogStartProcedureChangedEvent event) { if (hasStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getStartProcedureType())); } } @@ -7177,7 +7178,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogFinishPositioningConfirmedEvent event) { if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); } } @@ -7185,7 +7187,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogFinishPositioningListChangedEvent event) { if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); } } @@ -7213,7 +7216,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogProtestStartTimeEvent event) { if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getProtestTime())); } } @@ -7221,7 +7225,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogAdditionalScoringInformationEvent event) { if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getLogicalTimePoint(), event.getAuthor(), + raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getType())); } } @@ -7267,8 +7272,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } windFixEvents.getFilteredEvents() - .forEach(event -> raceLog.add(new RaceLogWindFixEventImpl(event.getLogicalTimePoint(), - event.getAuthor(), raceLog.getCurrentPassId(), event.getWindFix(), event.isMagnetic()))); + .forEach(event -> raceLog.add(new RaceLogWindFixEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), + event.getWindFix(), event.isMagnetic()))); final TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis From 81b0725dacd5d97e719381a1e5ec2f64a396de81 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Mon, 19 Feb 2018 17:43:51 +0100 Subject: [PATCH 157/214] fix for settings change after preselect --- .../ui/client/shared/charts/WindChart.java | 122 +++++++++++++----- .../gwt/ui/client/shared/racemap/RaceMap.java | 8 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 8 +- 3 files changed, 103 insertions(+), 35 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java index b5259456c97..330dc9bc975 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java @@ -33,6 +33,7 @@ import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import com.google.gwt.core.client.GWT; import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.text.client.DateTimeFormatRenderer; @@ -84,6 +85,8 @@ public class WindChart extends AbstractRaceChart implements R private final ColorMapImpl colorMap; + private WindSource preselectFilter; + /** * @param selectedRaceIdentifier * if null, this chart won't update its contents automatically upon race selection change; @@ -225,53 +228,96 @@ public class WindChart extends AbstractRaceChart implements R private void updateVisibleSeries() { List currentSeries = Arrays.asList(chart.getSeries()); Set visibleSeries = new HashSet(currentSeries); - - if (settings.isShowWindDirectionsSeries()) { - for (Map.Entry e : windSourceDirectionSeries.entrySet()) { - Series series = e.getValue(); - if (settings.getWindDirectionSourcesToDisplay().contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); + + if (preselectFilter != null) { + boolean wasFound = forceSeriesSelection(visibleSeries, windSourceDirectionSeries); + if(forceSeriesSelection(visibleSeries, windSourceSpeedSeries)){ + wasFound = true; + } + if(wasFound){ + preselectFilter = null; + } + } else { + if (settings.isShowWindDirectionsSeries()) { + for (Map.Entry e : windSourceDirectionSeries.entrySet()) { + Series series = e.getValue(); + if (settings.getWindDirectionSourcesToDisplay().contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + } series.select(true); // ensures that the checkbox will be ticked + series.setVisible(true); //and that it is actually being rendered + } else { + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } } - } else { + } + } else { + for (Map.Entry e : windSourceDirectionSeries.entrySet()) { + Series series = e.getValue(); if (visibleSeries.contains(series)) { chart.removeSeries(series, false); } } } - } else { - for (Map.Entry e : windSourceDirectionSeries.entrySet()) { - Series series = e.getValue(); - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); + + if (settings.isShowWindSpeedSeries()) { + for (Map.Entry e : windSourceSpeedSeries.entrySet()) { + Series series = e.getValue(); + if (settings.getWindSpeedSourcesToDisplay().contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + } + series.select(true); // ensures that the checkbox will be ticked + series.setVisible(true); //and that it is actually being rendered + } else { + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + } else { + for (Map.Entry e : windSourceSpeedSeries.entrySet()) { + Series series = e.getValue(); + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } } } } - if (settings.isShowWindSpeedSeries()) { - for (Map.Entry e : windSourceSpeedSeries.entrySet()) { - Series series = e.getValue(); - if (settings.getWindSpeedSourcesToDisplay().contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); - series.select(true); // ensures that the checkbox will be ticked - } - } else { - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } + onResize(); + } + + private boolean forceSeriesSelection(Set visibleSeries, Map toProcess) { + boolean wasInResult = false; + for (Map.Entry e : toProcess.entrySet()) { + Series series = e.getValue(); + WindSource seriesSource = e.getKey(); + // add all of type, remove non matching ones + if (seriesSource.getType().equals(preselectFilter.getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, true, false); } - } - } else { - for (Map.Entry e : windSourceSpeedSeries.entrySet()) { - Series series = e.getValue(); + // preselet the matching one + if (preselectFilter.equals(e.getKey())) { + wasInResult = true; + series.select(true); // ensures that the checkbox will be ticked + series.setVisible(true, true); + GWT.log("Showing " + series.getName()); + } else { + series.select(false); + series.setVisible(false, true); + GWT.log("Hiding " + series.getName()); + } + } else { if (visibleSeries.contains(series)) { chart.removeSeries(series, false); } } } - onResize(); + return wasInResult; } /** @@ -441,6 +487,7 @@ public class WindChart extends AbstractRaceChart implements R */ @Override public void updateSettings(WindChartSettings newSettings) { + preselectFilter = null; boolean clearCacheAndReload = false; final Set oldWindSourceTypesToRequest = getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection(); if (newSettings.getResolutionInMilliseconds() != settings.getResolutionInMilliseconds()) { @@ -660,4 +707,19 @@ public class WindChart extends AbstractRaceChart implements R public String getId() { return windChartLifecycle.getComponentId(); } + + /** + * Forces the display of a specific windProvider preselected, and all of same type unselected. Will be disabled by either updateSettings or once the provider was properly found and shown + */ + public void showProvider(WindSource windprovider) { + WindSourceType type = windprovider.getType(); + Set windSpeedSourcesToDisplay = new HashSet<>(); + Set windDirectionSourcesToDisplay = new HashSet<>(); + windSpeedSourcesToDisplay.add(type); + windDirectionSourcesToDisplay.add(type); + WindChartSettings patched = new WindChartSettings(true,windSpeedSourcesToDisplay,true,windDirectionSourcesToDisplay,settings.getResolutionInMilliseconds()); + updateSettings(patched); + preselectFilter = windprovider; + updateVisibleSeries(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index bfc82b3754d..0df53c62807 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -429,7 +429,7 @@ public class RaceMap extends AbstractCompositeComponent impleme private final boolean showHeaderPanel; /** Callback to set the visibility of the wind chart. */ - private final Consumer setWindChartVisibleCallback; + private final Consumer showWindChartForProvider; private ManagedInfoWindow managedInfoWindow; private class AdvantageLineUpdater implements QuickRanksListener { @@ -459,7 +459,7 @@ public class RaceMap extends AbstractCompositeComponent impleme SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, RaceCompetitorSet raceCompetitorSet, StringMessages stringMessages, RegattaAndRaceIdentifier raceIdentifier, - RaceMapResources raceMapResources, boolean showHeaderPanel, QuickRanksDTOProvider quickRanksDTOProvider, Consumer setWindChartVisibleCallback) { + RaceMapResources raceMapResources, boolean showHeaderPanel, QuickRanksDTOProvider quickRanksDTOProvider, Consumer showWindChartForProvider) { super(parent, context); this.showHeaderPanel = showHeaderPanel; this.quickRanksDTOProvider = quickRanksDTOProvider; @@ -471,7 +471,7 @@ public class RaceMap extends AbstractCompositeComponent impleme this.errorReporter = errorReporter; this.timer = timer; this.isSimulationEnabled = true; - this.setWindChartVisibleCallback = setWindChartVisibleCallback; + this.showWindChartForProvider = showWindChartForProvider; timer.addTimeListener(this); raceMapImageManager = new RaceMapImageManager(raceMapResources); markDTOs = new HashMap(); @@ -2230,7 +2230,7 @@ public class RaceMap extends AbstractCompositeComponent impleme }); return container; } else { - windSourceNameAnchor.addClickHandler(event -> setWindChartVisibleCallback.accept(true)); + windSourceNameAnchor.addClickHandler(event -> showWindChartForProvider.accept(windSource)); } return vPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 2e6553bbbd7..bdc20fec074 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -32,6 +32,7 @@ import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.FleetDTO; @@ -223,7 +224,7 @@ public class RaceBoardPanel raceMap = new RaceMap(this, componentContext, raceMapLifecycle, defaultRaceMapSettings, sailingService, asyncActionsExecutor, errorReporter, timer, competitorSelectionProvider, raceCompetitorSet, stringMessages, selectedRaceIdentifier, - raceMapResources, /* showHeaderPanel */ true, quickRanksDTOProvider, this::setWindChartVisible) { + raceMapResources, /* showHeaderPanel */ true, quickRanksDTOProvider, this::showInWindChart) { private static final String INDENT_SMALL_CONTROL_STYLE = "indentsmall"; private static final String INDENT_BIG_CONTROL_STYLE = "indentbig"; @Override @@ -515,6 +516,11 @@ public class RaceBoardPanel public void setWindChartVisible(boolean visible) { setComponentVisible(leaderboardAndMapViewer, windChart, visible); } + + public void showInWindChart(WindSource windprovider) { + setComponentVisible(leaderboardAndMapViewer, windChart, true); + windChart.showProvider(windprovider); + } /** * Sets the collapsable panel for the competitor chart open or close, if in CASCADE view mode.
From 8471ae66d72f660a12c11a8a9cf59a62e5f729c1 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 19 Feb 2018 22:07:33 +0100 Subject: [PATCH 158/214] improved comments; aligned with coding conventions Change-Id: I90c0035403eab187e87f0e01b63126e4c7cd3d2f --- .../impl/PairingListTemplateImpl.java | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/java/com.sap.sse/src/com/sap/sse/pairinglist/impl/PairingListTemplateImpl.java b/java/com.sap.sse/src/com/sap/sse/pairinglist/impl/PairingListTemplateImpl.java index 4309f19acb6..27c5ce39696 100644 --- a/java/com.sap.sse/src/com/sap/sse/pairinglist/impl/PairingListTemplateImpl.java +++ b/java/com.sap.sse/src/com/sap/sse/pairinglist/impl/PairingListTemplateImpl.java @@ -195,10 +195,14 @@ public class PairingListTemplateImpl implements PairingListTemplate { } /** - * Divides the allSeeds array into parts with seed combinations that starts with the same seeds. If there are to less - * flights this method cuts the off Seeds into 4000 parts to avoid to less Tasks. - * @param equalSeeds number of equal seeds at the beginning of seed combinations which should be grouped. Attention: If this number is to big there will be very many little parts. - * @param allSeeds array with seed combinations, which should be divided + * Divides the allSeeds array into parts with seed combinations that starts with the same seeds. The parts + * are sized according to the thread pool size used so as to achieve reasonable concurrency. + * + * @param equalSeeds + * number of equal seeds at the beginning of seed combinations which should be grouped. Attention: If + * this number is to big there will be very many little parts. + * @param allSeeds + * array with seed combinations, which should be divided * @return an Arraylist, that contains parts of allSeeds. */ private ArrayList divideSeeds(int[][] allSeeds) { @@ -217,8 +221,11 @@ public class PairingListTemplateImpl implements PairingListTemplate { /** * Fast sorting algorithm to sort a huge amount of seeds in max n*log(n) time. Uses counting sort. - * @param allSeeds array of seeds, that should be sorted - * @param competitorCount highest value inside the complete array. Needed for counting sort. + * + * @param allSeeds + * array of seeds, that should be sorted + * @param competitorCount + * highest value inside the complete array. Needed for counting sort. * @return sorted array with seed combinations */ protected int[][] radixSort(int[][] allSeeds, int competitorCount) { @@ -229,7 +236,6 @@ public class PairingListTemplateImpl implements PairingListTemplate { return allSeeds; } - //TODO use comparator private int[][] countSort(int[][] allSeeds, int i, int competitorCount) { int[][] output = new int[allSeeds.length][allSeeds[0].length]; int[] count = new int[competitorCount]; @@ -239,7 +245,6 @@ public class PairingListTemplateImpl implements PairingListTemplate { for (int z = 1; z < count.length; z++) { count[z] += count[z - 1]; } - //TODO change to for each for (int z = allSeeds.length - 1; z >= 0; z--) { output[--count[allSeeds[z][i]]] = allSeeds[z]; } @@ -474,18 +479,21 @@ public class PairingListTemplateImpl implements PairingListTemplate { } /** - * Switches competitors inside a group to improve competitor allocations. - * Method does not change the order of groups or flights. The standard deviation - * of team associations will not be influenced by this method. After executing the - * method the assignment should be well distributed. + * Switches competitors inside a group to improve competitor allocations. Method does not change the order of groups + * or flights. The standard deviation of team associations will not be influenced by this method. After executing + * the method the assignment should be well distributed. * - * @param pairinglist current pairing list - * @param flights count of flights - * @param groups count of groups - * @param competitors count of competitors + * @param pairinglist + * current pairing list + * @param flights + * count of flights + * @param groups + * count of groups + * @param competitors + * count of competitors * @return improved pairing list template */ - protected int[][] improveCompetitorAllocations(int[][] pairinglist, int flights, int groups, int competitors) { + protected int[][] improveCompetitorAllocations(final int[][] pairinglist, int flights, int groups, int competitors) { int[][] assignments = this.getAssignmentAssociations(pairinglist, new int[competitors][competitors / groups]); double neededAssigments = flights / (competitors / groups); double bestDev = Double.POSITIVE_INFINITY; @@ -531,8 +539,9 @@ public class PairingListTemplateImpl implements PairingListTemplate { } } } - if (this.calcStandardDev(pairinglist) < bestDev) { - bestDev = this.calcStandardDev(pairinglist); + final double standardDev = this.calcStandardDev(pairinglist); + if (standardDev < bestDev) { + bestDev = standardDev; bestPLT = pairinglist; } } @@ -541,13 +550,17 @@ public class PairingListTemplateImpl implements PairingListTemplate { } /** - * Improves the Assignment changes between two flights by switching the groups inside a flight. - * Competitors and there assignment will not be changed. - * @param pairingList complete pairingListTemplate which needs to be improved - * @param flights number of flights which was used to generate the pairingListTemplate - * @param competitors number of competitors which was used to generate the pairingListTemplate + * Improves the Assignment changes between two flights by switching the groups inside a flight. Competitors and + * their assignment will not be changed. * - * @return the improved pairingListTemplate + * @param pairingList + * complete pairingListTemplate which needs to be improved; will be modified in place and then returned + * @param flights + * number of flights which was used to generate the pairingListTemplate + * @param competitors + * number of competitors which was used to generate the pairingListTemplate + * + * @return the improved pairingListTemplate; identical to the {@code pairingList} parameter and returned only for convenience */ private int[][] improveAssignmentChanges(int[][] pairingList, int flights, int competitors) { int boatChanges[] = new int[flights - 1]; @@ -564,10 +577,10 @@ public class PairingListTemplateImpl implements PairingListTemplate { bestMatchesIndex = j; } } - if (bestMatchesIndex > 0) { + if (bestMatchesIndex > 0) { // for ==0 nothing needs to be done; the group that matches the last group + // of the previous flight best is already the first in its flight int[] temp = new int[pairingList[0].length]; System.arraycopy(pairingList[i * pairingList.length / flights], 0, temp, 0, temp.length); - System.arraycopy(pairingList[i * pairingList.length / flights + bestMatchesIndex], 0, pairingList[i * pairingList.length / flights], 0, groupNext.length); System.arraycopy(temp, 0, pairingList[i * pairingList.length / flights + bestMatchesIndex], 0, @@ -645,7 +658,9 @@ public class PairingListTemplateImpl implements PairingListTemplate { */ private boolean contains(int[] arr, int value) { for (int i = 0; i < arr.length; i++) { - if (arr[i] == value) return true; + if (arr[i] == value) { + return true; + } } return false; } From 361cb3c97a712b13cc40125842b6400b34b023a5 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 00:16:09 +0100 Subject: [PATCH 159/214] use LOW_POINT instead of HIGH_POINT as the default for Expedition all-in-one import Change-Id: Idbcb16492341fa0239f98daec5eb1d598dd807f2 --- .../gateway/trackfiles/impl/ExpeditionAllInOneImporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 0020e45a5a2..e8b3219b45b 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -147,7 +147,7 @@ public class ExpeditionAllInOneImporter { final String seriesName = Series.DEFAULT_NAME; // TODO wild guess... - final ScoringSchemeType scoringSchemeType = ScoringSchemeType.HIGH_POINT; + final ScoringSchemeType scoringSchemeType = ScoringSchemeType.LOW_POINT; final RankingMetrics rankingMetric = RankingMetrics.ONE_DESIGN; final int[] discardThresholds = new int[0]; From fedfbd2ad8b77bd25b122a708e28662f71e32095 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Tue, 20 Feb 2018 09:42:57 +0100 Subject: [PATCH 160/214] ensure new state is rendered if using multiple forcedSelections --- .../sailing/gwt/ui/client/shared/charts/WindChart.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java index 330dc9bc975..8dbe728794b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java @@ -230,13 +230,8 @@ public class WindChart extends AbstractRaceChart implements R Set visibleSeries = new HashSet(currentSeries); if (preselectFilter != null) { - boolean wasFound = forceSeriesSelection(visibleSeries, windSourceDirectionSeries); - if(forceSeriesSelection(visibleSeries, windSourceSpeedSeries)){ - wasFound = true; - } - if(wasFound){ - preselectFilter = null; - } + forceSeriesSelection(visibleSeries, windSourceDirectionSeries); + forceSeriesSelection(visibleSeries, windSourceSpeedSeries); } else { if (settings.isShowWindDirectionsSeries()) { for (Map.Entry e : windSourceDirectionSeries.entrySet()) { From ff412b1708f1bb8c6614f3643bfc5db8cd8455a7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 11:27:51 +0100 Subject: [PATCH 161/214] fixed typo in identifier Change-Id: I07153c67b4daf56d6a55c27398a6a108bd613dba --- .../impl/TracTracRaceTrackerImpl.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java index 6deab6e2ad5..9da600daf5d 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java @@ -317,21 +317,21 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl } // check if there is a directory configured where stored data files can be cached // only cache files for races in REPLAY state - final URI effectifeStoredURI; // may be altered by legacy caching mechanism; won't affect this tracker's ID + final URI effectiveStoredURI; // may be altered by legacy caching mechanism; won't affect this tracker's ID if ( (raceStatus != null && raceStatus.equals(TracTracConnectionConstants.REPLAY_STATUS)) || (raceVisibility != null && raceVisibility.equals(TracTracConnectionConstants.REPLAY_VISIBILITY)) ) { - effectifeStoredURI = checkForCachedStoredData(storedURI); + effectiveStoredURI = checkForCachedStoredData(storedURI); } else { - effectifeStoredURI = storedURI; + effectiveStoredURI = storedURI; } logger.info("Starting race tracker: " + tractracRace.getName() + " " + paramURL + " " + liveURI + " " - + effectifeStoredURI + " startOfTracking:" + + effectiveStoredURI + " startOfTracking:" + (startOfTracking != null ? startOfTracking.asMillis() : "n/a") + " endOfTracking:" + (endOfTracking != null ? endOfTracking.asMillis() : "n/a")); // Initialize data controller using live and stored data sources ISubscriberFactory subscriberFactory = SubscriptionLocator.getSusbcriberFactory(); - eventSubscriber = subscriberFactory.createEventSubscriber(tractracEvent, liveURI, effectifeStoredURI); + eventSubscriber = subscriberFactory.createEventSubscriber(tractracEvent, liveURI, effectiveStoredURI); competitorsListener = new ICompetitorsListener() { @Override public void updateCompetitor(ICompetitor competitor) { @@ -375,7 +375,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl eventSubscriber.subscribeRaces(racesListener); // Start live and stored data streams final Regatta effectiveRegatta; - raceSubscriber = subscriberFactory.createRaceSubscriber(tractracRace, liveURI, effectifeStoredURI); + raceSubscriber = subscriberFactory.createRaceSubscriber(tractracRace, liveURI, effectiveStoredURI); raceSubscriber.subscribeConnectionStatus(this); // Try to find a pre-associated event based on the Race ID if (regatta == null) { From 4fb9fe9efc935b284cd8a29a36da3c6372fde50f Mon Sep 17 00:00:00 2001 From: Maximilian Gross Date: Tue, 20 Feb 2018 10:53:42 +0000 Subject: [PATCH 162/214] added screenshots and edited the structure of the article --- .../eventmanagers/linking-race-videos.md | 96 +++++++++++-------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/wiki/howto/eventmanagers/linking-race-videos.md b/wiki/howto/eventmanagers/linking-race-videos.md index 54ca4f9a313..9b1b049b7a4 100644 --- a/wiki/howto/eventmanagers/linking-race-videos.md +++ b/wiki/howto/eventmanagers/linking-race-videos.md @@ -1,43 +1,63 @@ -1. Open the video on Youtube +# Link tracked races with their YouTube Live-Streams +This articel provides a short guidance of how to synchronize races with their YouTube Videos. + +## 1. Open the video on YouTube 1. Check first race covered by the video. - 1. Seek to race start. Ideally, the video shows a start countdown for this race. - 1. Note the video time for the start - 1. Take the Youtube URL to the clip board - 1. Check last race covered -1. Open Admin Console + 2. Seek to race start. Ideally, the video shows a start countdown for this race (see figure 1). + 3. Note the video time for the start + 4. Take the Youtube URL to the clip board + 5. Check last race covered + +![Figure 1: Most races provide a race start indicator](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/start_race_indicator.png) +**Figure 1: Most races provide a countdown which indicates when the race is going to start** + +## 2. Open Admin Console 1. This step is necessary to elevate you as admin user - 1. Open sapsailing.com - 1. Go to event home page - 1. In the URL, replace "http:///gwt/Home.html/...." by "http:///gwt/AdminConsole.html" - 1. Enter login credentials -1. In a new bowser tab - 1. Open the first race covered by the video - 1. Click "Manage Media" (only visible when logged into Admin Console before) - 1. Click Add - 1. Paste the video’s Youtube URL - 1. Wait for the dialog field to fill - 1. Adjust the “Name” field --> pattern, e.g.: Day - (Race ..). E.g.: + 2. Open sapsailing.com + 3. Go to event home page + 4. In the URL, replace "http://`server name>`/gwt/Home.html/...." by "http://`server name`/gwt/AdminConsole.html". _Remember to access the -master url!_ + 5. Enter login credentials + +## 3. In a new bowser tab + 1. Open the first race covered by the video. It is located under the _races_ tab and will redirect you to the raceboard. + 2. Click "Manage Media" (see figure 2, only visible when logged into Admin Console before) + 3. Click Add + 4. Paste the video’s Youtube URL + 5. Wait for the dialog field to fill + 6. Adjust the “Name” field --> pattern, e.g.: Day - (Race ..). E.g.: 1. “Live Replay – Day One – ESS Nice 2014 (Race 8..13)” 1. “3D Graphics Only – Day One – ESS Nice 2014 (Race 8..13)” - 1. Click OK - 1. A video window will pop up with the video a time 0:00. -1. In the video window + 7. Click OK + 8. A video window will pop up with the video a time 0:00. + +![Figure 2: Manage media button is only visible when logged in as admin](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/manage-media-btn.png) +**Figure 2: Manage media button is only visible when logged in as admin** + +## 4. In the video window 1. Click “Edit” (--> decouples video from race) - 1. Seek video to start of first race (if available, a count down overlay on the video is very helpful) - 1. Click “Preview” - 1. Start the race --> the video should start running - 1. Jump race to first mark rounding --> the video should show leader round first mark - 1. Check a few other distinct race points (mark rounding, finish, …). Be cautious with sections of 3D graphics embedded into live video: they have a delay and don't suit for alignment. - 1. For fine tuning press “Edit” again, use +/- buttons and confirm with “Preview” - 1. Click “Save”. If you forget to save, changes will not be stored on our server! - 1. Done. The video is linked to first race and synchronized. -1. While subsequent races covered by the same video should already be synchronized, they still need to be linked explicitly -1. Go to admin console - 1. Open tab “Races/Manage Media” - 1. Search the recently added video ( use search box) - 1. For this video, click column “Linked Race” (showing first race only) - 1. In the “Linked Races” dialog select all races covered by the video -1. Open each race and check a few distinct points (start, finish) -1. If a race doesn’t match the video - 1. decouple this race and all subsequent races (most likely, the video has been cut) - 1. start again from step 3 \ No newline at end of file + 2. Seek video to start of first race (see figure 3, if available, a count down overlay on the video is very helpful) + 3. Click “Preview” + 4. Start the race --> the video should start running + 5. Jump race to first mark rounding --> the video should show leader round first mark + 6. Check a few other distinct race points (mark rounding, finish, …). Be cautious with sections of 3D graphics embedded into live video: they have a delay and don't suit for alignment. + 7. For fine tuning press “Edit” again, use +/- buttons and confirm with “Preview” + 8. Click “Save”. If you forget to save, changes will not be stored on our server! + 9. Done. The video is linked to first race and synchronized. + 10. While subsequent races covered by the same video should already be synchronized, they still need to be linked explicitly + +![Figure 3: Synchronize the tracked race with the Live-Stream](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/sync_races.png) + +**Figure 3: Synchronize the tracked race with the Live-Stream** + +## 5. Go to Admin Console + 1. Open tab “Races/Manage Media” (see figure 4) + 2. Search the recently added video ( use search box) + 3. For this video, click column “Linked Race” (showing first race only) + 4. In the “Linked Races” dialog select all races covered by the video + 5. Open each race and check a few distinct points (start, finish) + 6. If a race doesn’t match the video + 7. decouple this race and all subsequent races (most likely, the video has been cut) + 8. start again from step 3. of chaper 1 + +![Figure 4: Link all races covered by the video](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/multi_sync.png) +**Figure 4: Link all races covered by the video** \ No newline at end of file From 389975c39e113151936950a7812c456edfa11715 Mon Sep 17 00:00:00 2001 From: Maximilian Gross Date: Tue, 20 Feb 2018 10:56:10 +0000 Subject: [PATCH 163/214] Updated linking race videos (markdown) --- wiki/howto/eventmanagers/linking-race-videos.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/wiki/howto/eventmanagers/linking-race-videos.md b/wiki/howto/eventmanagers/linking-race-videos.md index 9b1b049b7a4..bab7c8a7785 100644 --- a/wiki/howto/eventmanagers/linking-race-videos.md +++ b/wiki/howto/eventmanagers/linking-race-videos.md @@ -1,7 +1,6 @@ -# Link tracked races with their YouTube Live-Streams This articel provides a short guidance of how to synchronize races with their YouTube Videos. -## 1. Open the video on YouTube +# 1. Open the video on YouTube 1. Check first race covered by the video. 2. Seek to race start. Ideally, the video shows a start countdown for this race (see figure 1). 3. Note the video time for the start @@ -11,14 +10,14 @@ This articel provides a short guidance of how to synchronize races with their Yo ![Figure 1: Most races provide a race start indicator](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/start_race_indicator.png) **Figure 1: Most races provide a countdown which indicates when the race is going to start** -## 2. Open Admin Console +# 2. Open Admin Console 1. This step is necessary to elevate you as admin user 2. Open sapsailing.com 3. Go to event home page 4. In the URL, replace "http://`server name>`/gwt/Home.html/...." by "http://`server name`/gwt/AdminConsole.html". _Remember to access the -master url!_ 5. Enter login credentials -## 3. In a new bowser tab +# 3. In a new bowser tab 1. Open the first race covered by the video. It is located under the _races_ tab and will redirect you to the raceboard. 2. Click "Manage Media" (see figure 2, only visible when logged into Admin Console before) 3. Click Add @@ -33,7 +32,7 @@ This articel provides a short guidance of how to synchronize races with their Yo ![Figure 2: Manage media button is only visible when logged in as admin](https://s3-eu-west-1.amazonaws.com/media.sapsailing.com/wiki/how%20to/linking%20race%20videos/manage-media-btn.png) **Figure 2: Manage media button is only visible when logged in as admin** -## 4. In the video window +# 4. In the video window 1. Click “Edit” (--> decouples video from race) 2. Seek video to start of first race (see figure 3, if available, a count down overlay on the video is very helpful) 3. Click “Preview” @@ -49,7 +48,7 @@ This articel provides a short guidance of how to synchronize races with their Yo **Figure 3: Synchronize the tracked race with the Live-Stream** -## 5. Go to Admin Console +# 5. Go to Admin Console 1. Open tab “Races/Manage Media” (see figure 4) 2. Search the recently added video ( use search box) 3. For this video, click column “Linked Race” (showing first race only) From afafed669d505bfe20c8867d09ab4923362dee30 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 12:09:41 +0100 Subject: [PATCH 164/214] formatting; using separate variable for test verdict Change-Id: I22eeb12013084ce88cba21d65fac74011dd780b0 --- .../sap/sailing/gwt/ui/test/SlicingTest.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java index e723676be92..8a4b954e15b 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SlicingTest.java @@ -100,20 +100,15 @@ public class SlicingTest { final List> columnNames = new ArrayList<>(); columnNames.add(new Pair<>(columnNameOriginalRace, 0)); sailingService.addRaceColumnsToSeries(regatta.getRegattaIdentifier(), seriesName, columnNames); - sailingService.createRegattaLeaderboard(regatta.getRegattaIdentifier(), regattaName, new int[0]); - final List competitors = sailingService.addCompetitors(Arrays.asList(competitor), null); final CompetitorDTO competitorDTO = competitors.iterator().next(); sailingService.setCompetitorRegistrationsInRegattaLog(regattaName, new HashSet<>(competitors)); - DeviceIdentifierDTO deviceId = new DeviceIdentifierDTO("FILE", UUID.randomUUID().toString()); final DeviceMappingDTO deviceMapping = new DeviceMappingDTO(deviceId, startOriginalRace.asDate(), endOriginalRace.asDate(), competitorDTO, null); sailingService.addDeviceMappingToRegattaLog(regattaName, deviceMapping); - final List> leaderboardRaceColumnFleetNames = new ArrayList<>(); leaderboardRaceColumnFleetNames.add(new Triple<>(regattaName, columnNameOriginalRace, fleetName)); - RaceLogSetTrackingTimesDTO raceLogSetTrackingTimes = new RaceLogSetTrackingTimesDTO(); raceLogSetTrackingTimes.leaderboardName = regattaName; raceLogSetTrackingTimes.authorName = "Test"; @@ -122,24 +117,18 @@ public class SlicingTest { raceLogSetTrackingTimes.newStartOfTracking = new TimePointSpecificationFoundInLogImpl(startOriginalRace); raceLogSetTrackingTimes.newEndOfTracking = new TimePointSpecificationFoundInLogImpl(endOriginalRace); sailingService.setTrackingTimes(raceLogSetTrackingTimes); - sailingService.startRaceLogTracking(leaderboardRaceColumnFleetNames , true, false); // TODO add wind fixes - StrippedLeaderboardDTO leaderboard = sailingService.getLeaderboard(regattaName); + final StrippedLeaderboardDTO leaderboard = sailingService.getLeaderboard(regattaName); final RaceColumnDTO columnOfOriginalRace = leaderboard.getRaceList().iterator().next(); final RaceDTO originalRace = columnOfOriginalRace.getRace(columnOfOriginalRace.getFleets().iterator().next()); final RegattaAndRaceIdentifier raceIdentifierOfOriginalRace = originalRace.getRaceIdentifier(); - final RegattaAndRaceIdentifier sliceRaceIdentifier = sailingService.sliceRace(raceIdentifierOfOriginalRace, columnNameSlicedRace, startSlicedRace, endSlicedRace); - - - leaderboard = sailingService.getLeaderboard(regattaName); - final RaceColumnDTO columnOfSlicedRace = leaderboard.getRaceColumnByName(columnNameSlicedRace); + final StrippedLeaderboardDTO leaderboard2 = sailingService.getLeaderboard(regattaName); + final RaceColumnDTO columnOfSlicedRace = leaderboard2.getRaceColumnByName(columnNameSlicedRace); final RaceDTO slicedRace = columnOfSlicedRace.getRace(columnOfOriginalRace.getFleets().iterator().next()); - Assert.assertEquals(slicedRace.getRaceIdentifier(), sliceRaceIdentifier); - final TrackedRaceDTO trackedRace = slicedRace.trackedRace; Assert.assertEquals(startSlicedRace.asDate(), trackedRace.startOfTracking); Assert.assertEquals(endSlicedRace.asDate(), trackedRace.endOfTracking); From fb37be0e9f103833b265ea35aa388702138be677 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 12:27:27 +0100 Subject: [PATCH 165/214] single point of exit pattern Change-Id: I085ce4a0cfa9a28501d607b297cdde4ead0b066f --- .../sailing/gwt/ui/server/SailingServiceImpl.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 1b0d1d2dc51..976aa82320c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -6992,17 +6992,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); final Leaderboard regattaLeaderboard = getService().getLeaderboardByName(raceIdentifier.getRegattaName()); final DynamicTrackedRace trackedRace = getService().getTrackedRace(raceIdentifier); + final boolean result; if (regatta == null || !(regattaLeaderboard instanceof RegattaLeaderboard) || trackedRace == null || trackedRace.getStartOfTracking() == null || !isSmartphoneTrackingEnabled(trackedRace)) { - return false; + result = false; + } else { + final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRace); + result = (raceColumnAndFleetOfRaceToSlice != null); // is the TrackedRace associated to the given RegattaLeaderboard? } - - final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRace); - if (raceColumnAndFleetOfRaceToSlice == null) { - // the TrackedRace is not associated to the given RegattaLeaderboard - return false; - } - return true; + return result; } private boolean isSmartphoneTrackingEnabled(DynamicTrackedRace trackedRace) { From 18fdfedb25744a015b2dcfc60beeb8eb5a206e69 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 12:31:15 +0100 Subject: [PATCH 166/214] deprecated GWT serialization default constructor Change-Id: I778f5df06d269fbf3fe70701c227116f7eac3dec --- .../sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java index 1e35d095559..cb9f8824ab6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SliceRacePreperationDTO.java @@ -21,8 +21,8 @@ public class SliceRacePreperationDTO implements IsSerializable { Util.addAll(alreadyUsedNames, this.alreadyUsedNames); } - protected SliceRacePreperationDTO() { - } + @Deprecated + SliceRacePreperationDTO() {} public String getProposedRaceName() { return proposedRaceName; From c4ec2d709ecd6952589710d00677705c57c9a401 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 12:36:34 +0100 Subject: [PATCH 167/214] added " ", "_" and "-" to the legal prefix characters for race splitting suggestions Change-Id: I4a2341b49adf7e077ae801a2fdcfb1f47253be9c --- .../java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 976aa82320c..0c69e41ac92 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7019,7 +7019,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S String prefix = null; int currentCount = 0; - final Pattern pattern = Pattern.compile("^([a-zA-Z]+)([0-9]+)$"); + final Pattern pattern = Pattern.compile("^([a-zA-Z_ -]+)([0-9]+)$"); final HashSet alreadyUsedRaceNames = new HashSet<>(); for (RaceColumn column : regattaLeaderboard.getRaceColumns()) { alreadyUsedRaceNames.add(column.getName()); From 8fa1873471cc6f6366f174b6ae65029ee4c96774 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 15:08:29 +0100 Subject: [PATCH 168/214] copy all wind sources during slicing that canBeStored() Change-Id: Ia1ecf55226d1a49cf57a069937c37d5733c77e5d --- .../gwt/ui/raceboard/SliceRaceHandler.java | 7 ++-- .../gwt/ui/server/SailingServiceImpl.java | 40 +++++++------------ 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java index 3ca06d467ec..de5fdc848b4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SliceRaceHandler.java @@ -69,6 +69,9 @@ public class SliceRaceHandler { private boolean canSlice = false; + /** + * Registers this handler as a zoom event handler on the {@code competitorRaceChart}. + */ public SliceRaceHandler(SailingServiceAsync sailingService, UserService userService, final ErrorReporter errorReporter, MultiCompetitorRaceChart competitorRaceChart, RegattaAndRaceIdentifier selectedRaceIdentifier, final String leaderboardGroupName, String leaderboardName, UUID eventId) { @@ -79,9 +82,7 @@ public class SliceRaceHandler { this.leaderboardGroupName = leaderboardGroupName; this.leaderboardName = leaderboardName; this.eventId = eventId; - styles.ensureInjected(); - sliceButtonUi = new Button(); sliceButtonUi.setStyleName(styles.sliceButtonBackgroundImage()); sliceButtonUi.setTitle(StringMessages.INSTANCE.sliceRace()); @@ -93,7 +94,6 @@ public class SliceRaceHandler { updateVisibility(); }); sliceButtonUi.addClickHandler((e) -> doSlice()); - sailingService.canSliceRace(selectedRaceIdentifier, new AsyncCallback() { @Override public void onSuccess(Boolean result) { @@ -107,7 +107,6 @@ public class SliceRaceHandler { updateVisibility(); } }); - final UserStatusEventHandler userStatusEventHandler = (user, preAuthenticated) -> { updateVisibility(); }; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 0c69e41ac92..b61555f4e9d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7004,19 +7004,20 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } private boolean isSmartphoneTrackingEnabled(DynamicTrackedRace trackedRace) { + boolean result = false; for (RaceLog raceLog : trackedRace.getAttachedRaceLogs()) { RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze(); if (raceLogTrackingState.isTracking()) { - return true; + result = true; + break; } } - return false; + return result; } @Override public SliceRacePreperationDTO prepareForSlicingOfRace(final RegattaAndRaceIdentifier raceIdentifier) { final Leaderboard regattaLeaderboard = getService().getLeaderboardByName(raceIdentifier.getRegattaName()); - String prefix = null; int currentCount = 0; final Pattern pattern = Pattern.compile("^([a-zA-Z_ -]+)([0-9]+)$"); @@ -7029,7 +7030,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S currentCount = Integer.parseInt(matcher.group(2)); } } - if (prefix == null) { prefix = "R"; } @@ -7057,28 +7057,21 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S || (endOfTrackingOfRaceToSlice != null && endOfTrackingOfRaceToSlice.before(sliceTo))) { throw new RuntimeException("The TimeRange to slice is not part of the race"); } - final RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) getService().getLeaderboardByName(raceIdentifier.getRegattaName()); - final Pair raceColumnAndFleetOfRaceToSlice = regattaLeaderboard.getRaceColumnAndFleet(trackedRaceToSlice); // RaceColumns in a RegattaLeaderboard are always RaceColumnInSeries instances final RaceColumnInSeries raceColumnOfRaceToSlice = (RaceColumnInSeries) raceColumnAndFleetOfRaceToSlice.getA(); final Fleet fleet = raceColumnAndFleetOfRaceToSlice.getB(); final Series series = raceColumnOfRaceToSlice.getSeries(); final RaceLog raceLogOfRaceToSlice = raceColumnOfRaceToSlice.getRaceLog(fleet); - getService().apply(new AddColumnToSeries(regattaIdentifier, series.getName(), newRaceColumnName)); final RaceColumn raceColumn = regattaLeaderboard.getRaceColumnByName(newRaceColumnName); - final RaceLog raceLog = raceColumn.getRaceLog(fleet); - final AbstractLogEventAuthor author = getService().getServerAuthor(); - final TimePoint startOfTracking = sliceFrom; final TimePoint endOfTracking = sliceTo; raceLog.add(new RaceLogStartOfTrackingEventImpl(startOfTracking, author, raceLog.getCurrentPassId())); raceLog.add(new RaceLogEndOfTrackingEventImpl(endOfTracking, author, raceLog.getCurrentPassId())); - final TimeRange timeRange = new TimeRangeImpl(sliceFrom, sliceTo); final StartTimeFinderResult startTimeFinderResult = new StartTimeFinder(getService(), raceLogOfRaceToSlice).analyze(); final TimePoint startTime = startTimeFinderResult.getStartTime(); @@ -7268,25 +7261,20 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } }); } - windFixEvents.getFilteredEvents() .forEach(event -> raceLog.add(new RaceLogWindFixEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getWindFix(), event.isMagnetic()))); - final TimePoint startTrackingTimePoint = MillisecondsTimePoint.now(); // this ensures that the events consistently have different timepoints to ensure a consistent result of the state analysis // that's why we can't just call adapter.denoteRaceForRaceLogTracking final TimePoint denotationTimePoint = startTrackingTimePoint.minus(1); raceLog.add(new RaceLogDenoteForTrackingEventImpl(denotationTimePoint, author, raceLog.getCurrentPassId(), trackedRaceName, regatta.getBoatClass(), UUID.randomUUID())); - raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); - try { getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - DynamicTrackedRace trackedRace = null; for (int i = 0; i < 100 ; i++) { trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); @@ -7296,20 +7284,20 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S Thread.sleep(100); } } - if (trackedRace == null) { throw new IllegalStateException("Could not obtain sliced race after 10s"); } - - for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources(WindSourceType.EXPEDITION)) { - final WindTrack windTrackToCopyFrom = trackedRaceToSlice.getOrCreateWindTrack(windSourceToCopy); - windTrackToCopyFrom.lockForRead(); - try { - for (Wind windToCopy : windTrackToCopyFrom.getFixes(startOfTracking, true, endOfTracking, true)) { - trackedRace.recordWind(windToCopy, windSourceToCopy); + for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources()) { + if (windSourceToCopy.canBeStored()) { + final WindTrack windTrackToCopyFrom = trackedRaceToSlice.getOrCreateWindTrack(windSourceToCopy); + windTrackToCopyFrom.lockForRead(); + try { + for (Wind windToCopy : windTrackToCopyFrom.getFixes(startOfTracking, true, endOfTracking, true)) { + trackedRace.recordWind(windToCopy, windSourceToCopy); + } + } finally { + windTrackToCopyFrom.unlockAfterRead(); } - } finally { - windTrackToCopyFrom.unlockAfterRead(); } } return trackedRace.getRaceIdentifier(); From df460eddf318f8ecb3d322603826f6908e0e8d0a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 20 Feb 2018 15:18:02 +0100 Subject: [PATCH 169/214] added read lock for reading the race log of the race to slice Change-Id: I520636a404fe53333be4f0eec629574a4176d78f --- .../gwt/ui/server/SailingServiceImpl.java | 329 +++++++++--------- 1 file changed, 167 insertions(+), 162 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index b61555f4e9d..6c53ab0bf8f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7096,170 +7096,175 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S // Only wind fixes in the new tracking interval as well as the best fallback fixes are added to the new RaceLog final LogEventTimeRangeWithFallbackFilter windFixEvents = new LogEventTimeRangeWithFallbackFilter<>( timeRange); - for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { - raceLogEvent.accept(new BaseRaceLogEventVisitor() { - @Override - public void visit(RaceLogDependentStartTimeEvent event) { - if (dependentStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogDependentStartTimeEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getDependentOnRaceIdentifier(), - event.getStartTimeDifference(), event.getNextStatus())); - } - } - - @Override - public void visit(RaceLogStartTimeEvent event) { - if (!dependentStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogStartTimeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getStartTime(), - event.getNextStatus())); - } - } - - @Override - public void visit(RaceLogRegisterCompetitorEvent event) { - raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getCompetitor())); - } - - @Override - public void visit(RaceLogWindFixEvent event) { - windFixEvents.addEvent(event); - } - - @Override - public void visit(RaceLogUseCompetitorsFromRaceLogEvent event) { - raceLog.add(new RaceLogUseCompetitorsFromRaceLogEventImpl(event.getCreatedAt(), event.getAuthor(), - event.getLogicalTimePoint(), UUID.randomUUID(), raceLog.getCurrentPassId())); - } - - @Override - public void visit(RaceLogCourseDesignChangedEvent event) { - raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getCourseDesign(), event.getCourseDesignerMode())); - } - - @Override - public void visit(RaceLogFlagEvent event) { - if (hasStartTime && isLatestPass(event) && !event.getLogicalTimePoint().after(sliceTo)) { - raceLog.add(new RaceLogFlagEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getUpperFlag(), - event.getLowerFlag(), event.isDisplayed())); - } - } - - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getCourseAreaId())); - } - - @Override - public void visit(RaceLogStartProcedureChangedEvent event) { - if (hasStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getStartProcedureType())); - } - } - - @Override - public void visit(RaceLogFinishPositioningConfirmedEvent event) { - if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - } - } - - @Override - public void visit(RaceLogFinishPositioningListChangedEvent event) { - if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - } - } - - @Override - public void visit(RaceLogFixedMarkPassingEvent event) { - if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getTimePointOfFixedPassing())) { - raceLog.add(new RaceLogFixedMarkPassingEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - event.getInvolvedBoats(), raceLog.getCurrentPassId(), - event.getTimePointOfFixedPassing(), event.getZeroBasedIndexOfPassedWaypoint())); - } - } - - @Override - public void visit(RaceLogSuppressedMarkPassingsEvent event) { - if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getLogicalTimePoint())) { - raceLog.add(new RaceLogSuppressedMarkPassingsEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - event.getInvolvedBoats(), raceLog.getCurrentPassId(), - event.getZeroBasedIndexOfFirstSuppressedWaypoint())); - } - } - - @Override - public void visit(RaceLogProtestStartTimeEvent event) { - if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getProtestTime())); - } - } - - @Override - public void visit(RaceLogAdditionalScoringInformationEvent event) { - if (hasFinishedTime && isLatestPass(event)) { - raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getType())); - } - } - - @Override - public void visit(RaceLogPathfinderEvent event) { - if (hasStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogPathfinderEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), - event.getPathfinderId())); - } - } - - @Override - public void visit(RaceLogGateLineOpeningTimeEvent event) { - if (hasStartTime && isLatestPass(event)) { - raceLog.add(new RaceLogGateLineOpeningTimeEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getGateLineOpeningTimes().getGateLaunchStopTime(), - event.getGateLineOpeningTimes().getGolfDownTime())); - } - } - - @Override - public void visit(RaceLogRaceStatusEvent event) { - if (isLatestPass(event) && !(event instanceof RaceLogDependentStartTimeEvent) - && !(event instanceof RaceLogStartTimeEvent)) { - if ((hasStartTime - && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber()) - || (hasFinishingTime && event.getNextStatus() == RaceLogRaceStatus.FINISHING) - || (hasFinishedTime && event.getNextStatus() == RaceLogRaceStatus.FINISHED)) { - new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), - event.getNextStatus()); + raceLogOfRaceToSlice.lockForRead(); + try { + for (RaceLogEvent raceLogEvent : raceLogOfRaceToSlice.getUnrevokedEvents()) { + raceLogEvent.accept(new BaseRaceLogEventVisitor() { + @Override + public void visit(RaceLogDependentStartTimeEvent event) { + if (dependentStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogDependentStartTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getDependentOnRaceIdentifier(), + event.getStartTimeDifference(), event.getNextStatus())); } } - } - - private boolean isLatestPass(RaceLogEvent event) { - return event.getPassId() == raceLogOfRaceToSlice.getCurrentPassId(); - } - }); + + @Override + public void visit(RaceLogStartTimeEvent event) { + if (!dependentStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogStartTimeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getStartTime(), + event.getNextStatus())); + } + } + + @Override + public void visit(RaceLogRegisterCompetitorEvent event) { + raceLog.add(new RaceLogRegisterCompetitorEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getCompetitor())); + } + + @Override + public void visit(RaceLogWindFixEvent event) { + windFixEvents.addEvent(event); + } + + @Override + public void visit(RaceLogUseCompetitorsFromRaceLogEvent event) { + raceLog.add(new RaceLogUseCompetitorsFromRaceLogEventImpl(event.getCreatedAt(), event.getAuthor(), + event.getLogicalTimePoint(), UUID.randomUUID(), raceLog.getCurrentPassId())); + } + + @Override + public void visit(RaceLogCourseDesignChangedEvent event) { + raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getCourseDesign(), event.getCourseDesignerMode())); + } + + @Override + public void visit(RaceLogFlagEvent event) { + if (hasStartTime && isLatestPass(event) && !event.getLogicalTimePoint().after(sliceTo)) { + raceLog.add(new RaceLogFlagEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getUpperFlag(), + event.getLowerFlag(), event.isDisplayed())); + } + } + + @Override + public void visit(RaceLogCourseAreaChangedEvent event) { + raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getCourseAreaId())); + } + + @Override + public void visit(RaceLogStartProcedureChangedEvent event) { + if (hasStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogStartProcedureChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getStartProcedureType())); + } + } + + @Override + public void visit(RaceLogFinishPositioningConfirmedEvent event) { + if (hasFinishedTime && isLatestPass(event)) { + raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + } + } + + @Override + public void visit(RaceLogFinishPositioningListChangedEvent event) { + if (hasFinishedTime && isLatestPass(event)) { + raceLog.add(new RaceLogFinishPositioningListChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + } + } + + @Override + public void visit(RaceLogFixedMarkPassingEvent event) { + if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getTimePointOfFixedPassing())) { + raceLog.add(new RaceLogFixedMarkPassingEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + event.getInvolvedBoats(), raceLog.getCurrentPassId(), + event.getTimePointOfFixedPassing(), event.getZeroBasedIndexOfPassedWaypoint())); + } + } + + @Override + public void visit(RaceLogSuppressedMarkPassingsEvent event) { + if (hasStartTime && isLatestPass(event) && timeRange.includes(event.getLogicalTimePoint())) { + raceLog.add(new RaceLogSuppressedMarkPassingsEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + event.getInvolvedBoats(), raceLog.getCurrentPassId(), + event.getZeroBasedIndexOfFirstSuppressedWaypoint())); + } + } + + @Override + public void visit(RaceLogProtestStartTimeEvent event) { + if (hasFinishedTime && isLatestPass(event)) { + raceLog.add(new RaceLogProtestStartTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getProtestTime())); + } + } + + @Override + public void visit(RaceLogAdditionalScoringInformationEvent event) { + if (hasFinishedTime && isLatestPass(event)) { + raceLog.add(new RaceLogAdditionalScoringInformationEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getType())); + } + } + + @Override + public void visit(RaceLogPathfinderEvent event) { + if (hasStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogPathfinderEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), + event.getPathfinderId())); + } + } + + @Override + public void visit(RaceLogGateLineOpeningTimeEvent event) { + if (hasStartTime && isLatestPass(event)) { + raceLog.add(new RaceLogGateLineOpeningTimeEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getGateLineOpeningTimes().getGateLaunchStopTime(), + event.getGateLineOpeningTimes().getGolfDownTime())); + } + } + + @Override + public void visit(RaceLogRaceStatusEvent event) { + if (isLatestPass(event) && !(event instanceof RaceLogDependentStartTimeEvent) + && !(event instanceof RaceLogStartTimeEvent)) { + if ((hasStartTime + && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber()) + || (hasFinishingTime && event.getNextStatus() == RaceLogRaceStatus.FINISHING) + || (hasFinishedTime && event.getNextStatus() == RaceLogRaceStatus.FINISHED)) { + new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), + event.getNextStatus()); + } + } + } + + private boolean isLatestPass(RaceLogEvent event) { + return event.getPassId() == raceLogOfRaceToSlice.getCurrentPassId(); + } + }); + } + } finally { + raceLogOfRaceToSlice.unlockAfterRead(); } windFixEvents.getFilteredEvents() .forEach(event -> raceLog.add(new RaceLogWindFixEventImpl(event.getCreatedAt(), From aa0fc6128523e56ee6ff1ec61eaa1aa89a17c850 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Tue, 20 Feb 2018 16:41:11 +0100 Subject: [PATCH 170/214] removed gwt log statements --- .../sap/sailing/gwt/ui/client/shared/charts/WindChart.java | 3 --- .../gwt/ui/client/shared/racemap/ManagedInfoWindow.java | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java index 8dbe728794b..69671f1af6b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java @@ -33,7 +33,6 @@ import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; -import com.google.gwt.core.client.GWT; import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.text.client.DateTimeFormatRenderer; @@ -300,11 +299,9 @@ public class WindChart extends AbstractRaceChart implements R wasInResult = true; series.select(true); // ensures that the checkbox will be ticked series.setVisible(true, true); - GWT.log("Showing " + series.getName()); } else { series.select(false); series.setVisible(false, true); - GWT.log("Hiding " + series.getName()); } } else { if (visibleSeries.contains(series)) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java index c172ff2f005..56a74bb491d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManagedInfoWindow.java @@ -1,6 +1,5 @@ package com.sap.sailing.gwt.ui.client.shared.racemap; -import com.google.gwt.core.shared.GWT; import com.google.gwt.maps.client.MapWidget; import com.google.gwt.maps.client.base.LatLng; import com.google.gwt.maps.client.overlays.InfoWindow; @@ -14,7 +13,7 @@ public class ManagedInfoWindow { private final InfoWindow infoWindow = InfoWindow.newInstance(infoWindowOptions); private final MapWidget map; - private final FooBarBlubbKeineAhnung container = new FooBarBlubbKeineAhnung(); + private final InfoWindowContent container = new InfoWindowContent(); private boolean shown = false; @@ -43,7 +42,7 @@ public class ManagedInfoWindow { } } - private class FooBarBlubbKeineAhnung extends SimplePanel { + private class InfoWindowContent extends SimplePanel { private boolean attached = false; @@ -51,7 +50,6 @@ public class ManagedInfoWindow { if (!attached) { this.attached = true; onAttach(); - GWT.log("++++++++++++ Attaching info window ++++++++++++"); } } @@ -59,7 +57,6 @@ public class ManagedInfoWindow { if (attached) { this.attached = false; onDetach(); - GWT.log("------------ Detaching info window ------------"); } } } From 7d27c9b08071a982e90b04275da0991be753cfef Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Tue, 20 Feb 2018 16:52:04 +0100 Subject: [PATCH 171/214] cleaned up dozens of redundant numberformat creations --- .../gwt/ui/client/shared/racemap/RaceMap.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 0df53c62807..99af6bc397a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -417,6 +417,7 @@ public class RaceMap extends AbstractCompositeComponent impleme private boolean orientationChangeInProgress; private final NumberFormat numberFormatOneDecimal = NumberFormat.getFormat("0.0"); + private final NumberFormat numberFormatNoDecimal = NumberFormat.getFormat("0"); /** * The competitor for which the advantage line is currently showing. Should this competitor's quick rank change, or @@ -1651,10 +1652,10 @@ public class RaceMap extends AbstractCompositeComponent impleme final Position startLineRightPosition = numberOfStartWaypointMarks < 2 ? null : courseDTO.getStartMarkPositions().get(1); if (courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), + numberFormatOneDecimal.format(courseDTO.startLineLengthInMeters), + numberFormatOneDecimal.format(Math.abs(courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), courseDTO.startLineAdvantageousSide.name().charAt(0)+courseDTO.startLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.startLineAdvantageInMeters))); + numberFormatOneDecimal.format(courseDTO.startLineAdvantageInMeters))); } else { startLineAdvantageText.delete(0, startLineAdvantageText.length()); } @@ -1684,10 +1685,10 @@ public class RaceMap extends AbstractCompositeComponent impleme final Position finishLineRightPosition = numberOfFinishWaypointMarks < 2 ? null : courseDTO.getFinishMarkPositions().get(1); if (courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), + numberFormatOneDecimal.format(courseDTO.finishLineLengthInMeters), + numberFormatOneDecimal.format(Math.abs(courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), courseDTO.finishLineAdvantageousSide.name().charAt(0)+courseDTO.finishLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.finishLineAdvantageInMeters))); + numberFormatOneDecimal.format(courseDTO.finishLineAdvantageInMeters))); } else { finishLineAdvantageText.delete(0, finishLineAdvantageText.length()); } @@ -1760,14 +1761,14 @@ public class RaceMap extends AbstractCompositeComponent impleme final StringBuilder sb = new StringBuilder(); sb.append(stringMessages.courseMiddleLine()); sb.append('\n'); - sb.append(NumberFormat.getFormat("0").format( + sb.append(numberFormatNoDecimal.format( Math.abs(position1DTO.getDistance(position2DTO).getMeters()))+stringMessages.metersUnit()); if (lastCombinedWindTrackInfoDTO != null) { final WindTrackInfoDTO windTrackAtLegMiddle = lastCombinedWindTrackInfoDTO.getCombinedWindOnLegMiddle(zeroBasedIndexOfStartWaypoint); if (windTrackAtLegMiddle != null && windTrackAtLegMiddle.windFixes != null && !windTrackAtLegMiddle.windFixes.isEmpty()) { WindDTO windAtLegMiddle = windTrackAtLegMiddle.windFixes.get(0); final double legBearingDeg = position1DTO.getBearingGreatCircle(position2DTO).getDegrees(); - final String diff = NumberFormat.getFormat("0.0").format( + final String diff = numberFormatOneDecimal.format( Math.min(Math.abs(windAtLegMiddle.dampenedTrueWindBearingDeg-legBearingDeg), Math.abs(windAtLegMiddle.dampenedTrueWindFromDeg-legBearingDeg))); sb.append(", "); @@ -2183,19 +2184,18 @@ public class RaceMap extends AbstractCompositeComponent impleme + " -> " + NumberFormat.getDecimalFormat().format(after.speedInKnots) + " " + stringMessages.knotsUnit() + ")")); if (maneuver.maneuverLossInMeters != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), NumberFormat.getFormat("0.0").format(maneuver.maneuverLossInMeters)+"m")); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), numberFormatOneDecimal.format(maneuver.maneuverLossInMeters)+"m")); } return vPanel; } private Widget getInfoWindowContent(final WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - NumberFormat numberFormat = NumberFormat.getFormat("0.0"); final VerticalPanel vPanel = new VerticalPanel(); final Anchor windSourceNameAnchor = new Anchor(WindSourceTypeFormatter.format(windSource, stringMessages)); vPanel.add(createInfoWindowLabelWithWidget(stringMessages.windSource(), windSourceNameAnchor)); vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormatOneDecimal.format(windDTO.dampenedTrueWindSpeedInKnots))); final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(windDTO.measureTimepoint); vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), timePoint.asDate().toString())); vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), windDTO.position.getAsDegreesAndDecimalMinutesWithCardinalPoints())); From 5aa006111bed771cf4f54c321cdd95976e168ebb Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 20 Feb 2018 17:17:13 +0100 Subject: [PATCH 172/214] Bug 4380: RaceLogRaceStatusEvent are now correctly added to the RaceLog of the sliced race --- .../com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 6c53ab0bf8f..2203000c298 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7251,9 +7251,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S && event.getNextStatus().getOrderNumber() <= RaceLogRaceStatus.RUNNING.getOrderNumber()) || (hasFinishingTime && event.getNextStatus() == RaceLogRaceStatus.FINISHING) || (hasFinishedTime && event.getNextStatus() == RaceLogRaceStatus.FINISHED)) { - new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), + raceLog.add(new RaceLogRaceStatusEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), - event.getNextStatus()); + event.getNextStatus())); } } } From b698bb1fcbe8de000bcaf9786fefc9a9b8fa8f4a Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 21 Feb 2018 13:49:27 +0100 Subject: [PATCH 173/214] Bug 4380: Changed race slicing and expedition all in one import to not use polling when waiting for the TrackedRace --- .../gwt/ui/server/SailingServiceImpl.java | 20 +++--- .../impl/ExpeditionAllInOneImporter.java | 22 +++---- .../server/util/WaitForTrackedRaceUtil.java | 64 +++++++++++++++++++ 3 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 2203000c298..502fd9ec141 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -335,6 +335,7 @@ import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.Maneuver; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.Track; import com.sap.sailing.domain.tracking.TrackedLeg; @@ -495,6 +496,7 @@ import com.sap.sailing.server.operationaltransformation.UpdateSeries; import com.sap.sailing.server.operationaltransformation.UpdateServerConfiguration; import com.sap.sailing.server.operationaltransformation.UpdateSpecificRegatta; import com.sap.sailing.server.simulation.SimulationService; +import com.sap.sailing.server.util.WaitForTrackedRaceUtil; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationResults; @@ -7278,19 +7280,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S author, raceLog.getCurrentPassId(), trackedRaceName, regatta.getBoatClass(), UUID.randomUUID())); raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); try { - getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, + final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - DynamicTrackedRace trackedRace = null; - for (int i = 0; i < 100 ; i++) { - trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - break; - } else { - Thread.sleep(100); - } - } + + // wait for the RaceDefinition to be created + raceHandle.getRace(); + + final DynamicTrackedRace trackedRace = WaitForTrackedRaceUtil.waitForTrackedRace(raceColumn, fleet, 10); if (trackedRace == null) { - throw new IllegalStateException("Could not obtain sliced race after 10s"); + throw new IllegalStateException("Could not obtain sliced race"); } for (WindSource windSourceToCopy : trackedRaceToSlice.getWindSources()) { if (windSourceToCopy.canBeStored()) { diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java index 0e7115ece48..c83d8ebad3b 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/ExpeditionAllInOneImporter.java @@ -49,6 +49,7 @@ import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.trackimport.DoubleVectorFixImporter; import com.sap.sailing.domain.trackimport.GPSFixImporter; import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.gateway.trackfiles.impl.ImportResultDTO.ErrorImportDTO; import com.sap.sailing.server.gateway.trackfiles.impl.ImportResultDTO.TrackImportDTO; @@ -60,6 +61,7 @@ import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; import com.sap.sailing.server.operationaltransformation.CreateLeaderboardGroup; import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; import com.sap.sailing.server.operationaltransformation.UpdateEvent; +import com.sap.sailing.server.util.WaitForTrackedRaceUtil; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TypeBasedServiceFinderFactory; import com.sap.sse.common.Util.Pair; @@ -249,23 +251,17 @@ public class ExpeditionAllInOneImporter { raceLog.add(new RaceLogStartTrackingEventImpl(startTrackingTimePoint, author, raceLog.getCurrentPassId())); - adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, + final RaceHandle raceHandle = adapter.startTracking(service, regattaLeaderboard, raceColumn, fleet, /* trackWind */ true, /* correctWindDirectionByMagneticDeclination */ true); - DynamicTrackedRace trackedRace = null; - for (int i = 0; i < 100 ; i++) { - trackedRace = (DynamicTrackedRace) raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - break; - } else { - Thread.sleep(100); - } + // wait for the RaceDefinition to be created + raceHandle.getRace(); + + final DynamicTrackedRace trackedRace = WaitForTrackedRaceUtil.waitForTrackedRace(raceColumn, fleet, 10); + if (trackedRace == null) { + throw new IllegalStateException("Could not obtain imported race"); } - if (trackedRace == null) { - throw new IllegalStateException("Could not obtain imported race after 10s"); - } - final WindImportResult windImportResult = new AbstractWindImporter.WindImportResult(); final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, windSourceId); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java new file mode 100644 index 00000000000..3d8a0b51763 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java @@ -0,0 +1,64 @@ +package com.sap.sailing.server.util; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.RaceColumnListenerWithDefaultAction; +import com.sap.sailing.domain.tracking.TrackedRace; + +public class WaitForTrackedRaceUtil { + + private WaitForTrackedRaceUtil() { + } + + /** + * Waits for a {@link TrackedRace} to show up for the given {@link RaceColumn} and {@link Fleet}. If no + * {@link TrackedRace} shows up within the given amount of seconds, null is returned. + */ + @SuppressWarnings("unchecked") + public static T waitForTrackedRace(RaceColumn raceColumn, Fleet fleet, int secondsToWait) { + final CompletableFuture future = new CompletableFuture<>(); + + final RaceColumnListener raceColumnListener = new WaitForTrackedRaceListener(fleet, future); + raceColumn.addRaceColumnListener(raceColumnListener); + final TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + raceColumn.removeRaceColumnListener(raceColumnListener); + return (T) trackedRace; + } + + try { + return (T) future.get(secondsToWait, TimeUnit.SECONDS); + } catch (Exception e) { + raceColumn.removeRaceColumnListener(raceColumnListener); + return null; + } + } + + private static class WaitForTrackedRaceListener implements RaceColumnListenerWithDefaultAction { + private static final long serialVersionUID = 1L; + + private final Fleet fleet; + private final CompletableFuture future; + + public WaitForTrackedRaceListener(Fleet fleet, CompletableFuture future) { + this.fleet = fleet; + this.future = future; + } + + @Override + public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleetOfLinkedRace, TrackedRace trackedRace) { + if (fleet == fleetOfLinkedRace) { + raceColumn.removeRaceColumnListener(this); + future.complete(trackedRace); + } + } + + @Override + public void defaultAction() { + } + } +} From c409b88462715728001581682ec4f0e5c8c42d3a Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 21 Feb 2018 14:36:23 +0100 Subject: [PATCH 174/214] Bug 4380: RaceLogCourseDesignChangedEvent are now sliced with a race to get a proper course design for training sessions --- .../sailing/gwt/ui/server/SailingServiceImpl.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 502fd9ec141..c3debeb39b3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7141,9 +7141,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void visit(RaceLogCourseDesignChangedEvent event) { - raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getCreatedAt(), - event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), - raceLog.getCurrentPassId(), event.getCourseDesign(), event.getCourseDesignerMode())); + // Only course changes up to the end of the sliced race are copied + // to be able to cut several training races where the course is changed + // between races in preparation for the next race. In this case the course + // for a sliced is the course that was valid at the time that race took place. + if (event.getLogicalTimePoint().before(sliceTo)) { + raceLog.add(new RaceLogCourseDesignChangedEventImpl(event.getCreatedAt(), + event.getLogicalTimePoint(), event.getAuthor(), UUID.randomUUID(), + raceLog.getCurrentPassId(), event.getCourseDesign(), event.getCourseDesignerMode())); + } } @Override From f8ee0b88637a92882879b52fe5dfb998f756a1ae Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 21 Feb 2018 15:32:31 +0100 Subject: [PATCH 175/214] Bug 4380: The RaceColumnListener used to wait for a sliced race to be attached to a RaceColumn is now marked as transient --- .../com/sap/sailing/server/util/WaitForTrackedRaceUtil.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java index 3d8a0b51763..92a586fed27 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/util/WaitForTrackedRaceUtil.java @@ -60,5 +60,10 @@ public class WaitForTrackedRaceUtil { @Override public void defaultAction() { } + + @Override + public boolean isTransient() { + return true; + } } } From 33bbe632cd84548580c8a792cea2658cd7acc87d Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Wed, 21 Feb 2018 16:31:04 +0100 Subject: [PATCH 176/214] fixed wrong size for flag image bug4456 --- .../sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index d99d30f2e24..d66404848c2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -16,7 +16,6 @@ import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.ImageResourceRenderer; import com.google.gwt.user.client.ui.Label; import com.google.gwt.view.client.CellPreviewEvent; import com.sap.sailing.domain.common.dto.CompetitorDTO; @@ -98,7 +97,6 @@ public class CompetitorTableWrapper"); sb.appendHtmlConstant(" "); } } From e1c89747b3d71b3fe83b649d691a94c605c0ac03 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Wed, 21 Feb 2018 17:07:20 +0100 Subject: [PATCH 177/214] fix for rectangle flags, and replaced with template --- .../ui/adminconsole/CompetitorTableWrapper.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index d66404848c2..b531117dd50 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -8,7 +8,9 @@ import java.util.List; import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.cell.client.SafeHtmlCell; import com.google.gwt.core.client.Callback; +import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.client.SafeHtmlTemplates; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.cellview.client.Column; @@ -48,6 +50,15 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; public class CompetitorTableWrapper> extends TableWrapper { private final LabeledAbstractFilterablePanel filterField; + private static final Template TEMPLATE = GWT.create(Template.class); + + interface Template extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("
") + SafeHtml image(String imageUri,int width, int height); + @SafeHtmlTemplates.Template("
") + SafeHtml imageWithTitle(String imageUri,int width, int height,String title); + } + public CompetitorTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, boolean multiSelection, boolean enablePager) { super(sailingService, stringMessages, errorReporter, multiSelection, enablePager, @@ -100,7 +111,7 @@ public class CompetitorTableWrapper"); + sb.append(TEMPLATE.imageWithTitle(flagImageURL, 18 ,12,competitor.getName())); sb.appendHtmlConstant(" "); } else { final ImageResource flagImageResource; @@ -110,7 +121,7 @@ public class CompetitorTableWrapper"); + sb.append(TEMPLATE.imageWithTitle(flagImageResource.getSafeUri().asString(), 18 ,12,competitor.getName())); sb.appendHtmlConstant(" "); } } From 21b994a23fa44d00578d7e82e39e9617f4d72af0 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 22 Feb 2018 13:29:14 +0100 Subject: [PATCH 178/214] added null-safety in case a WindFinder report is incomplete; fixed last time point issue for WindFinder connector Change-Id: I2069cea07b9cb8372f7370616091831c99e5d1b8 --- .../impl/WindFinderReportParser.java | 15 +++++++++++++-- .../impl/WindFinderWindTracker.java | 10 ++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java index 80b33180f24..c9a15cf3041 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderReportParser.java @@ -34,9 +34,17 @@ public class WindFinderReportParser { * @param position The position of the station from which messages are to be parsed */ Wind parse(Position position, JSONObject jsonOfSingleMeasurement) throws NumberFormatException, ParseException { - return new WindImpl(position, new MillisecondsTimePoint(dateFormat.parse(jsonOfSingleMeasurement.get("dtl").toString())), + final Wind result; + if (jsonOfSingleMeasurement.get("dtl") != null && + jsonOfSingleMeasurement.get("ws") != null && + jsonOfSingleMeasurement.get("wd") != null) { + result = new WindImpl(position, new MillisecondsTimePoint(dateFormat.parse(jsonOfSingleMeasurement.get("dtl").toString())), new KnotSpeedWithBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("ws").toString()), new DegreeBearingImpl(Double.parseDouble(jsonOfSingleMeasurement.get("wd").toString())).reverse())); + } else { + result = null; + } + return result; } /** @@ -45,7 +53,10 @@ public class WindFinderReportParser { Iterable parse(Position position, JSONArray jsonOfSeveralMeasurements) throws NumberFormatException, ParseException { final List result = new ArrayList<>(); for (final Object jsonOfSingleMeasurement : jsonOfSeveralMeasurements) { - result.add(parse(position, (JSONObject) jsonOfSingleMeasurement)); + final Wind wind = parse(position, (JSONObject) jsonOfSingleMeasurement); + if (wind != null) { + result.add(wind); + } } return result; } diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java index a3507e48d98..07cad18abb5 100755 --- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java +++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.util.HashSet; import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -50,7 +51,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { private final ScheduledFuture poller; - private TimePoint timePointOfLastMeasurement; + private WeakHashMap timePointOfLastMeasurement; /** * The set of all {@link ReviewedSpotsCollection}s delivered by the {@link #factory} when this tracker @@ -63,6 +64,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { this.trackedRace = trackedRace; this.factory = factory; this.allSpotCollections = factory.getReviewedSpotsCollections(/* cached */ false); // obtain fresh copy of all spots, updating cache + this.timePointOfLastMeasurement = new WeakHashMap<>(); this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this, /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS); } @@ -75,7 +77,7 @@ public class WindFinderWindTracker implements WindTracker, Runnable { try { final Iterable usefulSpots = getUsefulSpots(); for (final Spot usefulSpot : usefulSpots) { - final Iterable windFixes = usefulSpot.getAllMeasurementsAfter(timePointOfLastMeasurement); + final Iterable windFixes = usefulSpot.getAllMeasurementsAfter(timePointOfLastMeasurement.get(usefulSpot)); if (!Util.isEmpty(windFixes)) { final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId()); final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); @@ -84,8 +86,8 @@ public class WindFinderWindTracker implements WindTracker, Runnable { if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) { // avoid duplicates by adding the same fix again trackedRace.recordWind(wind, windSource); - if (timePointOfLastMeasurement == null || wind.getTimePoint().after(timePointOfLastMeasurement)) { - timePointOfLastMeasurement = wind.getTimePoint(); + if (!timePointOfLastMeasurement.containsKey(usefulSpot) || wind.getTimePoint().after(timePointOfLastMeasurement.get(usefulSpot))) { + timePointOfLastMeasurement.put(usefulSpot, wind.getTimePoint()); } } } From bf3aa86e7183394a3592ef4d7ad0132277a16224 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 22 Feb 2018 13:46:06 +0100 Subject: [PATCH 179/214] updated admin release notes for WindFinder integration (bug1301) Change-Id: If4a3d2317bc40ea8d18efee05f2dba4be7f3dc03 --- .../release_notes_admin.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index c0d9117a5ad..8e86b8c8784 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -22,6 +22,23 @@

Release Notes - Administration Console

+

February 2018

+
    +
  • There is an integration with WindFinder (https://www.windfinder.com) now. + In the Event edit dialog there is a new tab "WindFinder" where so-called "spot collection ID" can be entered. + Such spot collection IDs are negotiated and manually selected by the WindFinder team upon request. Talk to + the SAP Sailing Analytics team or to WindFinder directly to obtain one for your area.

    + When such spot collections are provided for an event, a "Weather Data" button will appear on the event + landing page. Races whose tracking is started with wind tracking included will obtain wind information + from the wind finder spots of the spot collections configured. When the wind arrows for those readings + appear on the map, the bubble displayed when clicking them also contains a link to the respective + WindFinder web page.

    + The links to the WindFinder page are sensitive to the event time. If it's a few days before the event, + a forecast link will be used. During the event the actual report page is shown. Outside of these dates + a general weather statistics page is shown for the venue. +

  • +
+

January 2018

  • The auto detection for the media add dialog was improved. It will now attempt to only load the parts of a video, that are required to parse the tags. Additionally if the server cannot reach a video, the client (browser) will make an attempt to load the video. This allows to analyze locally hosted videos without the need to upload them.
  • From 63a676bf6421846bbcfc3b582d766287d54368d1 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 22 Feb 2018 14:01:55 +0100 Subject: [PATCH 180/214] consolidated obtaining wind finder spots for an Event into EventWindFinderUtil Change-Id: Ia53fb1ca1c70f3311d08e04d74401fb0ce49419d --- .../common/client/EventWindFinderUtil.java | 46 +++++++++++++++++++ .../event/GetEventViewAction.java | 31 ++----------- .../gwt/ui/server/SailingServiceImpl.java | 22 ++------- .../sap/sailing/gwt/ui/shared/EventDTO.java | 2 +- 4 files changed, 54 insertions(+), 47 deletions(-) create mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java new file mode 100755 index 00000000000..ded63ccee02 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java @@ -0,0 +1,46 @@ +package com.sap.sailing.gwt.common.client; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import org.json.simple.parser.ParseException; + +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.common.windfinder.SpotDTO; +import com.sap.sailing.domain.windfinder.Spot; +import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; + +public class EventWindFinderUtil { + private static final Logger logger = Logger.getLogger(EventWindFinderUtil.class.getName()); + + /** + * Obtains wind finder spots for a given Event + * + * @param useCachedSpotsForTrackedRaces + * whether or not to use the cached set of wind finder spots for tracked races + */ + public Iterable getWindFinderSpotsToConsider(Event event, WindFinderTrackerFactory windFinderTrackerFactory, boolean useCachedSpotsForTrackedRaces) { + final List windFinderSpots = new ArrayList<>(); + for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { + try { + for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). + getSpots(/* cached */ true)) { + windFinderSpots.add(new SpotDTO(spot)); + } + } catch (IOException | ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spots for reviewed spot collection with ID "+spotsCollectionId); + } + } + for (final String spotIdFromTrackedRace : event.getAllFinderSpotIdsUsedByTrackedRacesInEvent()) { + try { + windFinderSpots.add(new SpotDTO(windFinderTrackerFactory.getSpotById(spotIdFromTrackedRace, /* cached */ true))); + } catch (IOException | ParseException | InterruptedException | ExecutionException e) { + logger.warning("Unable to determine WindFinder spot with ID "+spotIdFromTrackedRace); + } + } + return windFinderSpots; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java index 7d0fda1934e..264c9861ec1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetEventViewAction.java @@ -1,22 +1,15 @@ package com.sap.sailing.gwt.home.communication.event; -import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; -import org.json.simple.parser.ParseException; - import com.google.gwt.core.shared.GwtIncompatible; import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.common.dto.EventType; -import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.windfinder.Spot; +import com.sap.sailing.gwt.common.client.EventWindFinderUtil; import com.sap.sailing.gwt.home.communication.SailingAction; import com.sap.sailing.gwt.home.communication.SailingDispatchContext; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; @@ -70,26 +63,10 @@ public class GetEventViewAction implements SailingAction, IsClient dto.setOfficialWebsiteURL(event.getOfficialWebsiteURL() == null ? null : event.getOfficialWebsiteURL().toString()); URL sailorsInfoWebsiteURL = event.getSailorsInfoWebsiteURLOrFallback(context.getClientLocale()); dto.setSailorsInfoWebsiteURL(sailorsInfoWebsiteURL == null ? null : sailorsInfoWebsiteURL.toString()); - final List windFinderSpots = new ArrayList<>(); - for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { - try { - for (final Spot spot : context.getWindFinderTrackerFactory().getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). - getSpots(/* cached */ true)) { - windFinderSpots.add(new SpotDTO(spot)); - } - } catch (IOException | ParseException | InterruptedException | ExecutionException e) { - logger.warning("Unable to determine WindFinder spots for reviewed spot collection with ID "+spotsCollectionId); - } + if (context.getWindFinderTrackerFactory() != null) { + dto.setAllWindFinderSpotsUsedByEvent(new EventWindFinderUtil().getWindFinderSpotsToConsider(event, + context.getWindFinderTrackerFactory(), /* useCachedSpotsForTrackedRaces */ true)); } - for (final String spotIdFromTrackedRace : event.getAllFinderSpotIdsUsedByTrackedRacesInEvent()) { - try { - windFinderSpots.add(new SpotDTO(context.getWindFinderTrackerFactory().getSpotById(spotIdFromTrackedRace, /* cached */ true))); - } catch (IOException | ParseException | InterruptedException | ExecutionException e) { - logger.warning("Unable to determine WindFinder spot with ID "+spotIdFromTrackedRace); - } - } - dto.setAllWindFinderSpotsUsedByEvent(windFinderSpots); - dto.setHasMedia(HomeServiceUtil.hasMedia(event)); dto.setState(HomeServiceUtil.calculateEventState(event)); // bug2982: always show leaderboard and competitor analytics diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 1af9bf3ab6f..09ff3917e3c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -318,6 +318,7 @@ import com.sap.sailing.domain.windfinder.WindFinderTrackerFactory; import com.sap.sailing.expeditionconnector.ExpeditionDeviceConfiguration; import com.sap.sailing.expeditionconnector.ExpeditionSensorDeviceIdentifier; import com.sap.sailing.expeditionconnector.ExpeditionTrackerFactory; +import com.sap.sailing.gwt.common.client.EventWindFinderUtil; import com.sap.sailing.gwt.server.HomeServiceUtil; import com.sap.sailing.gwt.ui.adminconsole.RaceLogSetTrackingTimesDTO; import com.sap.sailing.gwt.ui.client.SailingService; @@ -3925,26 +3926,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } eventDTO.setWindFinderReviewedSpotsCollection(event.getWindFinderReviewedSpotsCollectionIds()); final WindFinderTrackerFactory windFinderTrackerFactory = windFinderTrackerFactoryServiceTracker.getService(); - final List windFinderSpots = new ArrayList<>(); if (windFinderTrackerFactory != null) { - for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { - try { - for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). - getSpots(/* cached */ false)) { - windFinderSpots.add(new SpotDTO(spot)); - } - } catch (IOException | org.json.simple.parser.ParseException | InterruptedException | ExecutionException e) { - logger.warning("Unable to determine WindFinder spots for reviewed spot collection with ID "+spotsCollectionId); - } - } - for (final String spotIdFromTrackedRace : event.getAllFinderSpotIdsUsedByTrackedRacesInEvent()) { - try { - windFinderSpots.add(new SpotDTO(windFinderTrackerFactory.getSpotById(spotIdFromTrackedRace, /* cached */ false))); - } catch (IOException | org.json.simple.parser.ParseException | InterruptedException | ExecutionException e) { - logger.warning("Unable to determine WindFinder spot with ID "+spotIdFromTrackedRace); - } - } - eventDTO.setAllWindFinderSpotsUsedByEvent(windFinderSpots); + eventDTO.setAllWindFinderSpotsUsedByEvent(new EventWindFinderUtil().getWindFinderSpotsToConsider(event, + windFinderTrackerFactory, /* useCachedSpotsForTrackedRaces */ false)); } return eventDTO; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java index b8fc96041f3..dc3d607824b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventDTO.java @@ -135,7 +135,7 @@ public class EventDTO extends EventBaseDTO { return result; } - public void setAllWindFinderSpotsUsedByEvent(List windFinderSpots) { + public void setAllWindFinderSpotsUsedByEvent(Iterable windFinderSpots) { this.allWindFinderSpotIdsUsedByEvent = new ArrayList<>(); if (windFinderSpots != null) { Util.addAll(windFinderSpots, this.allWindFinderSpotIdsUsedByEvent); From 13cbeaa77610f7f2a55a07cba2645b9f29004860 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 22 Feb 2018 14:39:23 +0100 Subject: [PATCH 181/214] cleaned up admin release notes regarding duplicate WindFinder explanation Change-Id: I04b9f270e64ce20d93b8efcfa28cc28f8d602e06 --- java/com.sap.sailing.www/release_notes_admin.html | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index 8e86b8c8784..c3581707a74 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -45,17 +45,6 @@
  • In the raceboard the floating video box was improved, leading to a more solid resizing and moving behavior.
  • In the raceboard, the adding of videos has now an additional fallback startDate, if no startOfRace could be determined, and the file did not contain a proper creationtime tag, startOfTracking is used.
  • In the raceboard, the floating video box will now only display an edit button in the header, next to close and minimize. Upon clicking it, the box will expand and show the prior always shown editing buttons. This saves a lot of screenspace for logged in admin enabled users. -
  • Events have now an additional "WindFinder" tab in their configuration dialog. - WindFinder can provide reviewed subsets of their measurement spots and - gives an ID for such a reviewed spot collection. Enter one or more of such - IDs into your event configuration if you'd like to include WindFinder - (https://www.windfinder.com) as an additional wind source for races that - you track. Note that when tracking with live wind, all wind finder spots - from all events available on your server will be considered for inclusion, - based largely on their distance from your event's venue. Note further that - a WindFinder-enabled event will show a "WindFinder" button on the event - web page through which users can navigate to the statistics, forecasts - and reports of the respective measurement spots.

December 2017

From cde3ac9210de62d5339e06a86861e38e7149ed02 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Thu, 22 Feb 2018 15:45:04 +0100 Subject: [PATCH 182/214] Moved BeatAngle calculations to trackedrace level, as no leg values are used anyway. In the chart is is rendered nearly pixel perfect. changed English text Adjusted test cases to use the new method. --- .../impl/data/BravoFixWithContext.java | 5 ++- .../impl/data/FoilingSegmentWithContext.java | 3 +- .../sailing/domain/test/BeatAngleTest.java | 16 +++++----- .../tracking/TrackedLegOfCompetitor.java | 14 --------- .../sailing/domain/tracking/TrackedRace.java | 31 +++++++++++++++++++ .../impl/TrackedLegOfCompetitorImpl.java | 22 ------------- .../gwt/ui/client/StringMessages.properties | 2 +- .../gwt/ui/server/SailingServiceImpl.java | 7 ++--- .../mining/GPSFixMovingWithPolarContext.java | 12 +++---- 9 files changed, 51 insertions(+), 61 deletions(-) diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java index 5fc563fb059..6b0bd5ab28c 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java @@ -4,7 +4,6 @@ import com.sap.sailing.datamining.data.HasBravoFixContext; import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; @@ -64,8 +63,8 @@ public class BravoFixWithContext implements HasBravoFixContext { } @Override - public Bearing getAbsoluteTrueWindAngle() throws NoWindException { - return getTrackedRace().getTrackedLeg(getCompetitor(), getTimePoint()).getBeatAngle(getTimePoint()).abs(); + public Bearing getAbsoluteTrueWindAngle() { + return getTrackedRace().determineBeatAngleForChart(getCompetitor(), getTimePoint()).abs(); } private Competitor getCompetitor() { diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java index 402d123aec7..12120e1ed44 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java @@ -107,7 +107,8 @@ public class FoilingSegmentWithContext implements HasFoilingSegmentContext { } private Bearing getAbsoluteTrueWindAngle(final TimePoint timePoint) throws NoWindException { - return getTrackedRace().getTrackedLeg(getCompetitor(), timePoint).getBeatAngle(timePoint).abs(); + Bearing beatAngle = getTrackedRace().determineBeatAngleForChart(getCompetitor(), timePoint); + return beatAngle == null ? null:beatAngle.abs(); } @Override diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java index a3d2ac886fc..546359cba19 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java @@ -62,8 +62,8 @@ public class BeatAngleTest { new WindImpl(new DegreePosition(0, 0), MillisecondsTimePoint.now(), new KnotSpeedWithBearingImpl(12, windToBearing))); when(trackedLegOfCompetitor.getSpeedOverGround((TimePoint) anyObject())).thenReturn(new KnotSpeedWithBearingImpl(10, boatToBearing)); - when(trackedLegOfCompetitor.getBeatAngle((TimePoint) anyObject())).thenCallRealMethod(); - when(trackedLegOfCompetitor.getBeatAngle((TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); + when(trackedRace.determineBeatAngleForChart(competitor, (TimePoint) anyObject())).thenCallRealMethod(); + when(trackedRace.getBeatAngleFor(competitor, (TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); } @Test @@ -72,7 +72,7 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(348); final Bearing legBearing = new DegreeBearingImpl(303); setUp(windToBearing, boatToBearing, legBearing, LegType.UPWIND); - assertEquals(-45., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-45., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test @@ -81,7 +81,7 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(258); final Bearing legBearing = new DegreeBearingImpl(303); setUp(windToBearing, boatToBearing, legBearing, LegType.UPWIND); - assertEquals(45., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(45., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test @@ -90,7 +90,7 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(120); final Bearing legBearing = new DegreeBearingImpl(123); setUp(windToBearing, boatToBearing, legBearing, LegType.DOWNWIND); - assertEquals(-177., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-177., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test @@ -99,7 +99,7 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(126); final Bearing legBearing = new DegreeBearingImpl(123); setUp(windToBearing, boatToBearing, legBearing, LegType.DOWNWIND); - assertEquals(177., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(177., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test @@ -108,7 +108,7 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(33); final Bearing legBearing = new DegreeBearingImpl(33); setUp(windToBearing, boatToBearing, legBearing, LegType.REACHING); - assertEquals(-90., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-90., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test @@ -117,6 +117,6 @@ public class BeatAngleTest { final Bearing boatToBearing = new DegreeBearingImpl(33); final Bearing legBearing = new DegreeBearingImpl(33); setUp(windToBearing, boatToBearing, legBearing, LegType.REACHING); - assertEquals(90., trackedLegOfCompetitor.getBeatAngle(MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(90., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java index dad0788d027..ce01ff6f962 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java @@ -10,7 +10,6 @@ import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; import com.sap.sse.common.Duration; @@ -297,19 +296,6 @@ public interface TrackedLegOfCompetitor extends Serializable { TrackedLeg getTrackedLeg(); - /** - * Computes the angle between the competitors direction and the wind's "from" direction. The angle's direction is chosen such that - * it can be added to the boat's course over ground to arrive at the wind's {@link Wind#getFrom() "from"} direction. Example: wind - * from the north (0deg), boat's course over ground 90deg (moving east), then the bearing returned is -90deg. - */ - Bearing getBeatAngle(TimePoint at) throws NoWindException; - - /** - * Same as {@link #getBeatAngle}, only that additionally a cache is provided that can allow the method to use - * cached wind and leg type values. - */ - Bearing getBeatAngle(TimePoint at, WindLegTypeAndLegBearingCache cache) throws NoWindException; - /** * Like {@link #getAverageVelocityMadeGood(TimePoint)}, only with an additional cache argument that allows the method to * use already computed values for wind and leg type, potentially also updating the cache as it goes. diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index 1aca3e3f7ed..b681b90ec53 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -1075,6 +1075,36 @@ public interface TrackedRace extends Serializable, IsManagedByCache sogTrack = this.getTrack(competitor); + if (sogTrack != null) { + SpeedWithBearing speedOverGround = sogTrack.getEstimatedSpeed(timePoint); + Bearing projectToBearing; + Wind wind = cache.getWind(this, competitor, timePoint); + if (wind != null) { + projectToBearing = wind.getFrom(); + if (speedOverGround != null) { + beatAngle = speedOverGround.getBearing().getDifferenceTo(projectToBearing); + } + } + } + return beatAngle; + } + + /** + * Same as {@link #getBeatAngle}, only that additionally a cache is provided that can allow the method to use + * cached wind and leg type values. + */ + default Bearing determineBeatAngleForChart(Competitor competitor, TimePoint at){ + return getBeatAngleFor(competitor, at, new LeaderboardDTOCalculationReuseCache(at)); + }; /** * Like {@link #getVelocityMadeGood(Competitor, TimePoint)}, but allowing callers to specify a cache that can * accelerate requests for wind directions, the leg type and the competitor's current leg's bearing. @@ -1092,4 +1122,5 @@ public interface TrackedRace extends Serializable, IsManagedByCache computeTrueWindAngleAbsolute() { BearingWithConfidenceImpl result = null; Bearing bearing = null; - try { - TrackedLegOfCompetitor currentLeg = race.getCurrentLeg(competitor, fix.getTimePoint()); - if (currentLeg != null) { - Bearing realBearing = currentLeg.getBeatAngle(fix.getTimePoint()); - bearing = realBearing == null ? null : new DegreeBearingImpl(Math.abs(realBearing.getDegrees())); - } - } catch (NoWindException e) { - bearing = null; + TrackedLegOfCompetitor currentLeg = race.getCurrentLeg(competitor, fix.getTimePoint()); + if (currentLeg != null) { + Bearing realBearing = race.determineBeatAngleForChart(competitor, fix.getTimePoint()); + bearing = realBearing == null ? null : new DegreeBearingImpl(Math.abs(realBearing.getDegrees())); } WindWithConfidence> wind = race.getWindWithConfidence(fix.getPosition(), fix.getTimePoint()); From 4d1504165431fc77a7f24b0aa119fbbc2457a767 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 23 Feb 2018 10:35:12 +0100 Subject: [PATCH 183/214] Bug 4380: Also copying MediaTracks when slicing a race --- .../sailing/gwt/ui/server/SailingServiceImpl.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index e8e19597ef6..57137913bd5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -7342,6 +7342,20 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } } + + final Iterable mediaTracksForOriginalRace = getService().getMediaTracksForRace(raceIdentifier); + for (MediaTrack mediaTrack : mediaTracksForOriginalRace) { + if (mediaTrack.overlapsWith(sliceFrom, sliceTo)) { + final Set assignedRaces = new HashSet<>(mediaTrack.assignedRaces); + assignedRaces.add(trackedRace.getRaceIdentifier()); + // we can't just use the original instance and add the Race due to the fact that this leads to + // assignedRaces being empty afterwards. + final MediaTrack mediaTrackToSave = new MediaTrack(mediaTrack.dbId, mediaTrack.title, + mediaTrack.url, mediaTrack.startTime, mediaTrack.duration, mediaTrack.mimeType, + assignedRaces); + getService().mediaTrackAssignedRacesChanged(mediaTrackToSave); + } + } return trackedRace.getRaceIdentifier(); } catch (Exception e) { throw new RuntimeException("Error while slicing race", e); From 110b25e59ccba9694a8c045a022fb5e7d7160e81 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Fri, 23 Feb 2018 15:09:26 +0100 Subject: [PATCH 184/214] removed possible duplications for SpotDTOs --- .../sap/sailing/gwt/common/client/EventWindFinderUtil.java | 6 +++--- .../java/com/sap/sailing/gwt/ui/client/StringMessages.java | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java index ded63ccee02..659150d961c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/EventWindFinderUtil.java @@ -1,8 +1,8 @@ package com.sap.sailing.gwt.common.client; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; @@ -23,7 +23,7 @@ public class EventWindFinderUtil { * whether or not to use the cached set of wind finder spots for tracked races */ public Iterable getWindFinderSpotsToConsider(Event event, WindFinderTrackerFactory windFinderTrackerFactory, boolean useCachedSpotsForTrackedRaces) { - final List windFinderSpots = new ArrayList<>(); + final Set windFinderSpots = new LinkedHashSet<>(); for (final String spotsCollectionId : event.getWindFinderReviewedSpotsCollectionIds()) { try { for (final Spot spot : windFinderTrackerFactory.getReviewedSpotsCollectionById(spotsCollectionId, /* lookupInCache */ true). diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 13430e55fb0..f3d621b3185 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2058,6 +2058,5 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String enterIdOfWindfinderReviewedSpotCollection(); String enterTagsForTheImage(); String unableToResolveWindFinderSpotId(String id, String message); - String windFinderWeatherData(); } From f505b10d504432173f52705bd1d77eece78d2f87 Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Fri, 23 Feb 2018 17:06:14 +0100 Subject: [PATCH 185/214] removed RaceLogCourseAreaChangedEvent --- .../impl/DomainObjectFactoryImpl.java | 9 ---- .../impl/MongoObjectFactoryImpl.java | 16 ------ .../impl/MongoRaceLogStoreVisitor.java | 7 --- .../race/RaceLogCourseAreaChangedEvent.java | 9 ---- .../abstractlog/race/RaceLogEventVisitor.java | 2 - .../impl/AbstractRaceLogChangedVisitor.java | 5 -- .../race/impl/BaseRaceLogEventVisitor.java | 6 --- .../RaceLogCourseAreaChangeEventImpl.java | 36 ------------- .../impl/RaceColumnRaceLogReplicator.java | 6 --- .../gwt/ui/server/SailingServiceImpl.java | 8 --- .../test/StoreAndLoadRaceLogEventsTest.java | 16 ------ ...ingAndRetrievingRaceLogInLeaderboards.java | 23 -------- ...tStoringAndRetrievingRaceLogInRegatta.java | 25 --------- ...LogCourseAreaChangedEventDeserializer.java | 32 ----------- .../impl/RaceLogEventDeserializer.java | 7 --- ...ceLogCourseAreaChangedEventSerializer.java | 35 ------------ .../racelog/impl/RaceLogEventSerializer.java | 10 ---- .../CourseAreaChangedEventSerializerTest.java | 53 ------------------- .../racelog/RaceLogEventSerializerTest.java | 13 +---- 19 files changed, 1 insertion(+), 317 deletions(-) delete mode 100644 java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogCourseAreaChangedEvent.java delete mode 100644 java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogCourseAreaChangeEventImpl.java delete mode 100644 java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogCourseAreaChangedEventDeserializer.java delete mode 100644 java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogCourseAreaChangedEventSerializer.java delete mode 100644 java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/CourseAreaChangedEventSerializerTest.java diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index efed552b20c..817a62fd46e 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -42,7 +42,6 @@ import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.CompetitorResult.MergeState; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -65,7 +64,6 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultImpl; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultsImpl; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogDependentStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; @@ -1405,8 +1403,6 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { return loadRaceLogFlagEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); } else if (eventClass.equals(RaceLogPassChangeEvent.class.getSimpleName())) { return loadRaceLogPassChangeEvent(createdAt, author, logicalTimePoint, id, passId, competitors); - } else if (eventClass.equals(RaceLogCourseAreaChangedEvent.class.getSimpleName())) { - return loadRaceLogCourseAreaChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); } else if (eventClass.equals(RaceLogCourseDesignChangedEvent.class.getSimpleName())) { return loadRaceLogCourseDesignChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); } else if (eventClass.equals(RaceLogFinishPositioningListChangedEvent.class.getSimpleName())) { @@ -1568,11 +1564,6 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { return new RaceLogCourseDesignChangedEventImpl(createdAt, logicalTimePoint, author, id, passId, courseData, courseDesignerMode); } - private RaceLogCourseAreaChangedEvent loadRaceLogCourseAreaChangedEvent(TimePoint createdAt, AbstractLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - Serializable courseAreaId = (Serializable) dbObject.get(FieldNames.COURSE_AREA_ID.name()); - return new RaceLogCourseAreaChangeEventImpl(createdAt, logicalTimePoint, author, id, passId, courseAreaId); - } - private RaceLogEvent loadRaceLogFixedMarkPassingEvent(TimePoint createdAt, AbstractLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { TimePoint ofFixedPassing = loadTimePoint(dbObject, FieldNames.TIMEPOINT_OF_FIXED_MARKPASSING); diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 94b46b0451c..bdf4558bd65 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -28,7 +28,6 @@ import com.mongodb.util.JSON; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.CompetitorResult; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -846,13 +845,6 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { return result; } - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseAreaChangedEvent(courseAreaChangedEvent)); - return result; - } - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { BasicDBObject result = new BasicDBObject(); storeRaceLogIdentifier(raceLogIdentifier, result); @@ -1180,14 +1172,6 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { return result; } - private DBObject storeRaceLogCourseAreaChangedEvent(RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(courseAreaChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseAreaChangedEvent.class.getSimpleName()); - result.put(FieldNames.COURSE_AREA_ID.name(), courseAreaChangedEvent.getCourseAreaId()); - return result; - } - private DBObject storeRaceLogCourseDesignChangedEvent(RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { DBObject result = new BasicDBObject(); storeRaceLogEventProperties(courseDesignChangedEvent, result); diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java index a65b20e14ad..4ba962744a7 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java @@ -3,7 +3,6 @@ package com.sap.sailing.domain.persistence.impl; import java.util.logging.Logger; import com.mongodb.DBObject; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -68,12 +67,6 @@ public class MongoRaceLogStoreVisitor implements RaceLogEventVisitor { mongoObjectFactory.storeRaceLogEventEvent(startTimeEventTrackEntry); } - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - DBObject courseAreaChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(courseAreaChangedEventTrackEntry); - } - @Override public void visit(RaceLogCourseDesignChangedEvent event) { DBObject courseDesignChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogCourseAreaChangedEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogCourseAreaChangedEvent.java deleted file mode 100644 index b63b82fd432..00000000000 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogCourseAreaChangedEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sap.sailing.domain.abstractlog.race; - -import java.io.Serializable; - -public interface RaceLogCourseAreaChangedEvent extends RaceLogEvent { - - Serializable getCourseAreaId(); - -} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java index 572f66310d7..a8d8af89858 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java @@ -18,8 +18,6 @@ public interface RaceLogEventVisitor { public void visit(RaceLogStartTimeEvent event); - public void visit(RaceLogCourseAreaChangedEvent event); - public void visit(RaceLogCourseDesignChangedEvent event); public void visit(RaceLogFinishPositioningListChangedEvent event); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java index 8407d6f2d86..55f3e0be220 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java @@ -1,6 +1,5 @@ package com.sap.sailing.domain.abstractlog.race.impl; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -46,10 +45,6 @@ public abstract class AbstractRaceLogChangedVisitor implements RaceLogEventVisit notifyListenerAboutEventAdded(event); } - public void visit(RaceLogCourseAreaChangedEvent event) { - notifyListenerAboutEventAdded(event); - } - public void visit(RaceLogCourseDesignChangedEvent event) { notifyListenerAboutEventAdded(event); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java index bff059adf00..9aa980bb7d9 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java @@ -1,6 +1,5 @@ package com.sap.sailing.domain.abstractlog.race.impl; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -51,11 +50,6 @@ public abstract class BaseRaceLogEventVisitor implements RaceLogEventVisitor { } - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - - } - @Override public void visit(RaceLogCourseDesignChangedEvent event) { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogCourseAreaChangeEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogCourseAreaChangeEventImpl.java deleted file mode 100644 index 92ea38896d4..00000000000 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogCourseAreaChangeEventImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sap.sailing.domain.abstractlog.race.impl; - -import java.io.Serializable; - -import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor; -import com.sap.sse.common.TimePoint; - -public class RaceLogCourseAreaChangeEventImpl extends RaceLogEventImpl implements RaceLogCourseAreaChangedEvent { - private static final long serialVersionUID = -3943108136024977860L; - - private final Serializable courseAreaId; - - public RaceLogCourseAreaChangeEventImpl(TimePoint createdAt, TimePoint pTimePoint, AbstractLogEventAuthor author, - Serializable pId, int pPassId, Serializable courseAreaId) { - super(createdAt, pTimePoint, author, pId, pPassId); - this.courseAreaId = courseAreaId; - } - - public RaceLogCourseAreaChangeEventImpl(TimePoint logicalTimePoint, AbstractLogEventAuthor author, int pPassId, - Serializable courseAreaId) { - this(now(), logicalTimePoint, author, randId(), pPassId, courseAreaId); - } - - @Override - public Serializable getCourseAreaId() { - return courseAreaId; - } - - @Override - public void accept(RaceLogEventVisitor visitor) { - visitor.visit(this); - } - -} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java index 505a5cc97b7..26c2a045b4c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java @@ -2,7 +2,6 @@ package com.sap.sailing.domain.base.impl; import java.io.Serializable; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -49,11 +48,6 @@ public class RaceColumnRaceLogReplicator implements RaceLogEventVisitor, Seriali event); } - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - notifyOnAdd(event); - } - @Override public void visit(RaceLogStartTimeEvent event) { notifyOnAdd(event); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 57137913bd5..9fc5364feb2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -65,7 +65,6 @@ import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.AllEventsOfTypeFinder; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -93,7 +92,6 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderRes import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesEventFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TrackingTimesFinder; import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogDependentStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; @@ -7194,12 +7192,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - raceLog.add(new RaceLogCourseAreaChangeEventImpl(event.getCreatedAt(), event.getLogicalTimePoint(), - event.getAuthor(), UUID.randomUUID(), raceLog.getCurrentPassId(), event.getCourseAreaId())); - } - @Override public void visit(RaceLogStartProcedureChangedEvent event) { if (hasStartTime && isLatestPass(event)) { diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java index faf5c6d5423..4eb5c543b50 100644 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java @@ -20,14 +20,12 @@ import com.mongodb.DBObject; import com.mongodb.MongoException; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogPassChangeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogRevokeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFlagEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogPassChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; @@ -120,20 +118,6 @@ public class StoreAndLoadRaceLogEventsTest extends AbstractMongoDBTest { assertEquals(isDisplayed, actualEvent.isDisplayed()); } - @Test - public void testStoreAndLoadCourseAreaChangedEvent() { - Serializable courseAreaId = UUID.randomUUID(); - - RaceLogCourseAreaChangedEvent expectedEvent = new RaceLogCourseAreaChangeEventImpl(MillisecondsTimePoint.now(), - expectedEventTime, author, expectedId, expectedPassId, courseAreaId); - - DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); - RaceLogCourseAreaChangedEvent actualEvent = loadEvent(dbObject); - - assertBaseFields(expectedEvent, actualEvent); - assertEquals(courseAreaId, actualEvent.getCourseAreaId()); - } - @Test public void testStoreAndLoadPassChangeEvent() { RaceLogPassChangeEvent expectedEvent = new RaceLogPassChangeEventImpl(MillisecondsTimePoint.now(), diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInLeaderboards.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInLeaderboards.java index 7a28a46958a..5b7c550d75c 100644 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInLeaderboards.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInLeaderboards.java @@ -24,7 +24,6 @@ import com.sap.sailing.domain.abstractlog.race.CompetitorResult; import com.sap.sailing.domain.abstractlog.race.CompetitorResult.MergeState; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; @@ -42,7 +41,6 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultImpl; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultsImpl; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogDependentStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEventComparator; @@ -430,27 +428,6 @@ public class TestStoringAndRetrievingRaceLogInLeaderboards extends RaceLogMongoD } } - @Test - public void testStoreAndRetrieveSimpleLeaderboardWithRaceLogCourseAreaChangeEvent() { - final UUID uuid = UUID.randomUUID(); - RaceLogCourseAreaChangedEvent event = new RaceLogCourseAreaChangeEventImpl(now, author, 0, uuid); - addAndStoreRaceLogEvent(leaderboard, raceColumnName, event); - RaceLog loadedRaceLog = retrieveRaceLog(); - loadedRaceLog.lockForRead(); - try { - RaceLogEvent loadedEvent = loadedRaceLog.getFirstRawFix(); - RaceLogCourseAreaChangedEvent courseAreaEvent = (RaceLogCourseAreaChangedEvent) loadedEvent; - assertEquals(event.getCreatedAt(), courseAreaEvent.getCreatedAt()); - assertEquals(event.getLogicalTimePoint(), courseAreaEvent.getLogicalTimePoint()); - assertEquals(event.getPassId(), courseAreaEvent.getPassId()); - assertEquals(event.getId(), courseAreaEvent.getId()); - assertEquals(event.getCourseAreaId(), courseAreaEvent.getCourseAreaId()); - assertEquals(1, Util.size(loadedRaceLog.getFixes())); - } finally { - loadedRaceLog.unlockAfterRead(); - } - } - @Test public void testStoreAndRetrieveSimpleLeaderboardWithRaceLogRaceStatusEvent() { diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInRegatta.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInRegatta.java index f35ff25cace..6670ad26ebb 100644 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInRegatta.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingRaceLogInRegatta.java @@ -17,7 +17,6 @@ import com.mongodb.MongoException; import com.sap.sailing.domain.abstractlog.race.CompetitorResult.MergeState; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; @@ -33,7 +32,6 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultImpl; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultsImpl; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConfirmedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningListChangedEventImpl; @@ -298,29 +296,6 @@ public class TestStoringAndRetrievingRaceLogInRegatta extends AbstractTestStorin MongoObjectFactoryImpl factoryImpl = (MongoObjectFactoryImpl) mongoObjectFactory; factoryImpl.getRaceLogCollection().insert(raceLogResult); } - - @Test - public void testStoreAndRetrieveRegattaWithRaceLogCourseAreaChangeEvent() { - final UUID uuid = UUID.randomUUID(); - RaceLogCourseAreaChangedEvent event = new RaceLogCourseAreaChangeEventImpl(now, author, 0, uuid); - - addAndStoreRaceLogEvent(regatta, raceColumnName, event); - - RaceLog loadedRaceLog = retrieveRaceLog(); - - loadedRaceLog.lockForRead(); - try { - RaceLogEvent loadedEvent = loadedRaceLog.getFirstRawFix(); - RaceLogCourseAreaChangedEvent courseAreaEvent = (RaceLogCourseAreaChangedEvent) loadedEvent; - assertEquals(event.getLogicalTimePoint(), courseAreaEvent.getLogicalTimePoint()); - assertEquals(event.getPassId(), courseAreaEvent.getPassId()); - assertEquals(event.getId(), courseAreaEvent.getId()); - assertEquals(event.getCourseAreaId(), courseAreaEvent.getCourseAreaId()); - assertEquals(1, Util.size(loadedRaceLog.getFixes())); - } finally { - loadedRaceLog.unlockAfterRead(); - } - } @Test public void testStoreAndRetrieveRegattaWithRaceLogRaceStatusEvent() { diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogCourseAreaChangedEventDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogCourseAreaChangedEventDeserializer.java deleted file mode 100644 index 9820f070d6a..00000000000 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogCourseAreaChangedEventDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sap.sailing.server.gateway.deserialization.racelog.impl; - -import java.io.Serializable; -import java.util.List; - -import org.json.simple.JSONObject; - -import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.Helpers; -import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogCourseAreaChangedEventSerializer; -import com.sap.sse.common.TimePoint; - -public class RaceLogCourseAreaChangedEventDeserializer extends BaseRaceLogEventDeserializer { - - public RaceLogCourseAreaChangedEventDeserializer(JsonDeserializer competitorDeserializer) { - super(competitorDeserializer); - } - - @Override - protected RaceLogEvent deserialize(JSONObject object, Serializable id, TimePoint createdAt, AbstractLogEventAuthor author, TimePoint timePoint, int passId, List competitors) - throws JsonDeserializationException { - Serializable courseAreaId = (Serializable) object.get(RaceLogCourseAreaChangedEventSerializer.FIELD_COURSE_AREA_ID); - return new RaceLogCourseAreaChangeEventImpl(createdAt, timePoint, author, id, - passId, Helpers.tryUuidConversion(courseAreaId)); - } - -} diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogEventDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogEventDeserializer.java index 0962ae5f77f..048608e5046 100755 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogEventDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racelog/impl/RaceLogEventDeserializer.java @@ -20,7 +20,6 @@ import com.sap.sailing.server.gateway.deserialization.impl.PositionJsonDeseriali import com.sap.sailing.server.gateway.deserialization.impl.WindJsonDeserializer; import com.sap.sailing.server.gateway.serialization.racelog.impl.BaseRaceLogEventSerializer; import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogAdditionalScoringInformationSerializer; -import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogCourseAreaChangedEventSerializer; import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogCourseDesignChangedEventSerializer; import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogDenoteForTrackingEventSerializer; import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogDependentStartTimeEventSerializer; @@ -61,7 +60,6 @@ public class RaceLogEventDeserializer implements JsonDeserializer new RaceLogStartTimeEventDeserializer(competitorDeserializer), new RaceLogDependentStartTimeEventDeserializer(competitorDeserializer), new RaceLogRaceStatusEventDeserializer(competitorDeserializer), - new RaceLogCourseAreaChangedEventDeserializer(competitorDeserializer), new RaceLogCourseDesignChangedEventDeserializer(competitorDeserializer, new CourseBaseDeserializer( new WaypointDeserializer( @@ -93,7 +91,6 @@ public class RaceLogEventDeserializer implements JsonDeserializer protected final JsonDeserializer startTimeEventDeserializer; protected final JsonDeserializer dependentStartTimeEventDeserializer; protected final JsonDeserializer raceStatusEventDeserializer; - protected final JsonDeserializer courseAreaChangedEventDeserializer; protected final JsonDeserializer courseDesignChangedEventDeserializer; protected final JsonDeserializer finishPositioningListChangedEventDeserializer; protected final JsonDeserializer finishPositioningConfirmedEventDeserializer; @@ -118,7 +115,6 @@ public class RaceLogEventDeserializer implements JsonDeserializer JsonDeserializer startTimeEventDeserializer, JsonDeserializer dependentStartTimeEventDeserializer, JsonDeserializer raceStatusEventDeserializer, - JsonDeserializer courseAreaChangedEventDeserializer, JsonDeserializer courseDesignChangedEventDeserializer, JsonDeserializer finishPositioningListChangedEventDeserializer, JsonDeserializer finishPositioningConfirmedEventDeserializer, @@ -142,7 +138,6 @@ public class RaceLogEventDeserializer implements JsonDeserializer this.startTimeEventDeserializer = startTimeEventDeserializer; this.dependentStartTimeEventDeserializer = dependentStartTimeEventDeserializer; this.raceStatusEventDeserializer = raceStatusEventDeserializer; - this.courseAreaChangedEventDeserializer = courseAreaChangedEventDeserializer; this.courseDesignChangedEventDeserializer = courseDesignChangedEventDeserializer; this.finishPositioningListChangedEventDeserializer = finishPositioningListChangedEventDeserializer; this.finishPositioningConfirmedEventDeserializer = finishPositioningConfirmedEventDeserializer; @@ -177,8 +172,6 @@ public class RaceLogEventDeserializer implements JsonDeserializer return raceStatusEventDeserializer; } else if (type.equals(RaceLogPassChangeEventSerializer.VALUE_CLASS)) { return passChangeEventDeserializer; - } else if (type.equals(RaceLogCourseAreaChangedEventSerializer.VALUE_CLASS)) { - return courseAreaChangedEventDeserializer; } else if (type.equals(RaceLogCourseDesignChangedEventSerializer.VALUE_CLASS)) { return courseDesignChangedEventDeserializer; } else if (type.equals(RaceLogFinishPositioningListChangedEventSerializer.VALUE_CLASS)) { diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogCourseAreaChangedEventSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogCourseAreaChangedEventSerializer.java deleted file mode 100644 index 76c041c8f84..00000000000 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogCourseAreaChangedEventSerializer.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sap.sailing.server.gateway.serialization.racelog.impl; - -import org.json.simple.JSONObject; - -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.server.gateway.serialization.JsonSerializer; - -public class RaceLogCourseAreaChangedEventSerializer extends BaseRaceLogEventSerializer { - - public static final String VALUE_CLASS = RaceLogCourseAreaChangedEvent.class.getSimpleName(); - public static final String FIELD_COURSE_AREA_ID = "courseAreaId"; - - public RaceLogCourseAreaChangedEventSerializer( - JsonSerializer competitorSerializer) { - super(competitorSerializer); - } - - @Override - protected String getClassFieldValue() { - return VALUE_CLASS; - } - - @Override - public JSONObject serialize(RaceLogEvent object) { - RaceLogCourseAreaChangedEvent caChangedEvent = (RaceLogCourseAreaChangedEvent) object; - - JSONObject result = super.serialize(caChangedEvent); - result.put(FIELD_COURSE_AREA_ID, caChangedEvent.getCourseAreaId().toString()); - - return result; - } - -} diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogEventSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogEventSerializer.java index 68c6e58beae..70ac90e7c86 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogEventSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racelog/impl/RaceLogEventSerializer.java @@ -2,7 +2,6 @@ package com.sap.sailing.server.gateway.serialization.racelog.impl; import org.json.simple.JSONObject; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; @@ -54,7 +53,6 @@ public class RaceLogEventSerializer implements JsonSerializer, Rac new RaceLogFlagEventSerializer(competitorSerializer), new RaceLogStartTimeEventSerializer(competitorSerializer), new RaceLogRaceStatusEventSerializer(competitorSerializer), - new RaceLogCourseAreaChangedEventSerializer(competitorSerializer), new RaceLogPassChangeEventSerializer(competitorSerializer), new RaceLogCourseDesignChangedEventSerializer(competitorSerializer, new CourseBaseJsonSerializer( @@ -87,7 +85,6 @@ public class RaceLogEventSerializer implements JsonSerializer, Rac private final JsonSerializer flagEventSerializer; private final JsonSerializer startTimeSerializer; private final JsonSerializer raceStatusSerializer; - private final JsonSerializer courseAreaChangedEventSerializer; private final JsonSerializer passChangedEventSerializer; private final JsonSerializer courseDesignChangedEventSerializer; private final JsonSerializer finishPositioningListChangedEventSerializer; @@ -116,7 +113,6 @@ public class RaceLogEventSerializer implements JsonSerializer, Rac JsonSerializer flagEventSerializer, JsonSerializer startTimeSerializer, JsonSerializer raceStatusSerializer, - JsonSerializer courseAreaChangedEventSerializer, JsonSerializer passChangedEventSerializer, JsonSerializer courseDesignChangedEventSerializer, JsonSerializer finishPositioningListChangedEventSerializer, @@ -140,7 +136,6 @@ public class RaceLogEventSerializer implements JsonSerializer, Rac this.flagEventSerializer = flagEventSerializer; this.startTimeSerializer = startTimeSerializer; this.raceStatusSerializer = raceStatusSerializer; - this.courseAreaChangedEventSerializer = courseAreaChangedEventSerializer; this.passChangedEventSerializer = passChangedEventSerializer; this.courseDesignChangedEventSerializer = courseDesignChangedEventSerializer; this.finishPositioningListChangedEventSerializer = finishPositioningListChangedEventSerializer; @@ -201,11 +196,6 @@ public class RaceLogEventSerializer implements JsonSerializer, Rac chosenSerializer = startTimeSerializer; } - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - chosenSerializer = courseAreaChangedEventSerializer; - } - @Override public void visit(RaceLogCourseDesignChangedEvent event) { chosenSerializer = courseDesignChangedEventSerializer; diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/CourseAreaChangedEventSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/CourseAreaChangedEventSerializerTest.java deleted file mode 100644 index 184d86c0bd1..00000000000 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/CourseAreaChangedEventSerializerTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sap.sailing.server.gateway.serialization.test.racelog; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.Serializable; -import java.util.UUID; - -import org.json.simple.JSONObject; -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.server.gateway.serialization.JsonSerializer; -import com.sap.sailing.server.gateway.serialization.racelog.impl.RaceLogCourseAreaChangedEventSerializer; - -public class CourseAreaChangedEventSerializerTest extends AbstractEventSerializerTest { - - private Serializable expectedCourseAreaId; - - @Before - public void setUp() { - expectedCourseAreaId = UUID.randomUUID(); - super.setUp(); - } - - @Override - protected RaceLogCourseAreaChangedEvent createMockEvent() { - RaceLogCourseAreaChangedEvent event = mock(RaceLogCourseAreaChangedEvent.class); - when(event.getCourseAreaId()).thenReturn(expectedCourseAreaId); - return event; - } - - @Override - protected JsonSerializer createSerializer(JsonSerializer competitorSerializer) { - return new RaceLogCourseAreaChangedEventSerializer(competitorSerializer); - } - - @Test - public void testCourseAreaId() { - - JSONObject result = serializer.serialize(event); - - assertEquals( - expectedCourseAreaId.toString(), - result.get(RaceLogCourseAreaChangedEventSerializer.FIELD_COURSE_AREA_ID).toString()); - - } - -} diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogEventSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogEventSerializerTest.java index a6a52e35d98..ff8b0983862 100644 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogEventSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogEventSerializerTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseAreaChangeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogCourseDesignChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConfirmedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningListChangedEventImpl; @@ -44,7 +43,6 @@ public class RaceLogEventSerializerTest { private JsonSerializer flagEventSerializer; private JsonSerializer startTimeSerializer; private JsonSerializer raceStatusSerializer; - private JsonSerializer courseAreaChangedEventSerializer; private JsonSerializer passChangedSerializer; private JsonSerializer courseDesignChangedEventSerializer; private JsonSerializer finishPositioningListChangedEventSerializer; @@ -74,7 +72,6 @@ public class RaceLogEventSerializerTest { flagEventSerializer = mock(JsonSerializer.class); startTimeSerializer = mock(JsonSerializer.class); raceStatusSerializer = mock(JsonSerializer.class); - courseAreaChangedEventSerializer = mock(JsonSerializer.class); passChangedSerializer = mock(JsonSerializer.class); courseDesignChangedEventSerializer = mock(JsonSerializer.class); finishPositioningListChangedEventSerializer = mock(JsonSerializer.class); @@ -97,7 +94,7 @@ public class RaceLogEventSerializerTest { endOfTrackingEventSerializer = mock(JsonSerializer.class); serializer = new RaceLogEventSerializer(flagEventSerializer, startTimeSerializer, raceStatusSerializer, - courseAreaChangedEventSerializer, passChangedSerializer, courseDesignChangedEventSerializer, + passChangedSerializer, courseDesignChangedEventSerializer, finishPositioningListChangedEventSerializer, finishPositioningConfirmedEventSerializer, pathfinderEventSerializer, gateLineOpeningTimeEventSerializer, startProcedureTypeChangedEventSerializer, protestStartTimeEventSerializer, windFixEventSerializer, @@ -132,14 +129,6 @@ public class RaceLogEventSerializerTest { verify(raceStatusSerializer).serialize(event); } - @Test - public void testChoosesCourseAreaChangedSerializer() { - // we use the real event type here because we do not want to re-implement the dispatching. - RaceLogEvent event = new RaceLogCourseAreaChangeEventImpl(null, author, 0, null); - serializer.serialize(event); - verify(courseAreaChangedEventSerializer).serialize(event); - } - @Test public void testChoosesPassChangedSerializer() { // we use the real event type here because we do not want to re-implement the dispatching. From 524c51abe1d18d54184cc13c48b9eb36b64ac7fd Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Mon, 26 Feb 2018 09:52:49 +0100 Subject: [PATCH 186/214] prevented a possible npe, in case of invalid sogTrack, fixed unit test to properly mock new methods --- .../src/com/sap/sailing/domain/test/BeatAngleTest.java | 8 +++++--- .../src/com/sap/sailing/domain/tracking/TrackedRace.java | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java index 546359cba19..83b4bf1886b 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; +import org.mockito.Matchers; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.impl.CompetitorImpl; @@ -61,9 +62,10 @@ public class BeatAngleTest { when(trackedRace.getWind((Position) anyObject(), (TimePoint) anyObject())).thenReturn( new WindImpl(new DegreePosition(0, 0), MillisecondsTimePoint.now(), new KnotSpeedWithBearingImpl(12, windToBearing))); - when(trackedLegOfCompetitor.getSpeedOverGround((TimePoint) anyObject())).thenReturn(new KnotSpeedWithBearingImpl(10, boatToBearing)); - when(trackedRace.determineBeatAngleForChart(competitor, (TimePoint) anyObject())).thenCallRealMethod(); - when(trackedRace.getBeatAngleFor(competitor, (TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); + + when(competitorTrack.getEstimatedSpeed((TimePoint) anyObject())).thenReturn(new KnotSpeedWithBearingImpl(10, boatToBearing)); + when(trackedRace.determineBeatAngleForChart(Matchers.eq(competitor), (TimePoint) anyObject())).thenCallRealMethod(); + when(trackedRace.getBeatAngleFor(Matchers.eq(competitor), (TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); } @Test diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index b681b90ec53..c194fb39689 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -1086,12 +1086,10 @@ public interface TrackedRace extends Serializable, IsManagedByCache sogTrack = this.getTrack(competitor); if (sogTrack != null) { SpeedWithBearing speedOverGround = sogTrack.getEstimatedSpeed(timePoint); - Bearing projectToBearing; Wind wind = cache.getWind(this, competitor, timePoint); - if (wind != null) { - projectToBearing = wind.getFrom(); + if (wind != null && speedOverGround != null) { if (speedOverGround != null) { - beatAngle = speedOverGround.getBearing().getDifferenceTo(projectToBearing); + beatAngle = speedOverGround.getBearing().getDifferenceTo(wind.getFrom()); } } } From 4fbfa9433071ffc16d42db6e873e838f10ddf285 Mon Sep 17 00:00:00 2001 From: Wiki User Date: Mon, 26 Feb 2018 08:57:42 +0000 Subject: [PATCH 187/214] added sorting to stats calculations --- configuration/unique_ips_per_referrer_fast | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/configuration/unique_ips_per_referrer_fast b/configuration/unique_ips_per_referrer_fast index 0f017614ddd..eb09a917247 100755 --- a/configuration/unique_ips_per_referrer_fast +++ b/configuration/unique_ips_per_referrer_fast @@ -8,4 +8,9 @@ CACHE=$VARLOGOLD/cache/unique-ips-per-referrer JAR_FILE=$VARLOGOLD/com.sap.sse.jar -$JAVA_HOME/bin/java -jar $JAR_FILE $CACHE $* +$JAVA_HOME/bin/java -Xmx8G -jar $JAR_FILE $CACHE $* + +# Do the sorting of all .unique files now: +for i in `find $CACHE -name '*.unique'`; do + cat $i | awk '{ print $2 " " $1; }' | sort -rn >${i}.sorted +done From 00519d74f2b16e2150a243c2fe7391bd6038c29b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Feb 2018 10:09:20 +0100 Subject: [PATCH 188/214] re-introduced variable for clarity; removed redundant null check Change-Id: Ibe8cefa22bdd28b91aa32094d5ba0c0be6f74f5e --- .../src/com/sap/sailing/domain/tracking/TrackedRace.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index c194fb39689..d6069406567 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -1088,9 +1088,8 @@ public interface TrackedRace extends Serializable, IsManagedByCache Date: Mon, 26 Feb 2018 13:24:56 +0100 Subject: [PATCH 189/214] renamed to twa --- .../impl/data/BravoFixWithContext.java | 2 +- .../impl/data/FoilingSegmentWithContext.java | 4 +-- .../test/{BeatAngleTest.java => TWATest.java} | 30 +++++++++---------- .../sailing/domain/tracking/TrackedRace.java | 14 ++++----- .../gwt/ui/client/DetailTypeFormatter.java | 4 +-- .../sailing/gwt/ui/client/StringMessages.java | 4 +-- .../gwt/ui/client/StringMessages.properties | 4 +-- .../ui/client/StringMessages_de.properties | 4 +-- .../ui/client/StringMessages_es.properties | 4 +-- .../ui/client/StringMessages_fr.properties | 4 +-- .../ui/client/StringMessages_ja.properties | 4 +-- .../ui/client/StringMessages_pt.properties | 4 +-- .../ui/client/StringMessages_ru.properties | 4 +-- .../ui/client/StringMessages_zh.properties | 4 +-- .../datamining/presentation/ChartFactory.java | 4 +-- .../ManeuverSpeedDetailsResultsPresenter.java | 4 +-- .../ui/polarmining/PolarResultsPresenter.java | 2 +- .../mining/AngleAndSpeedRegression.java | 2 +- .../mining/GPSFixMovingWithPolarContext.java | 2 +- 19 files changed, 52 insertions(+), 52 deletions(-) rename java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/{BeatAngleTest.java => TWATest.java} (76%) diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java index 6b0bd5ab28c..3864d8144e0 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/BravoFixWithContext.java @@ -64,7 +64,7 @@ public class BravoFixWithContext implements HasBravoFixContext { @Override public Bearing getAbsoluteTrueWindAngle() { - return getTrackedRace().determineBeatAngleForChart(getCompetitor(), getTimePoint()).abs(); + return getTrackedRace().getTWA(getCompetitor(), getTimePoint()).abs(); } private Competitor getCompetitor() { diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java index 12120e1ed44..c3e38b5e479 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/FoilingSegmentWithContext.java @@ -107,8 +107,8 @@ public class FoilingSegmentWithContext implements HasFoilingSegmentContext { } private Bearing getAbsoluteTrueWindAngle(final TimePoint timePoint) throws NoWindException { - Bearing beatAngle = getTrackedRace().determineBeatAngleForChart(getCompetitor(), timePoint); - return beatAngle == null ? null:beatAngle.abs(); + Bearing twa = getTrackedRace().getTWA(getCompetitor(), timePoint); + return twa == null ? null:twa.abs(); } @Override diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TWATest.java similarity index 76% rename from java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java rename to java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TWATest.java index 83b4bf1886b..c4c079c5d46 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BeatAngleTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TWATest.java @@ -33,7 +33,7 @@ import com.sap.sailing.domain.tracking.impl.TrackedLegOfCompetitorImpl; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; -public class BeatAngleTest { +public class TWATest { private Competitor competitor; private GPSFixTrack competitorTrack; private TrackedRace trackedRace; @@ -64,61 +64,61 @@ public class BeatAngleTest { windToBearing))); when(competitorTrack.getEstimatedSpeed((TimePoint) anyObject())).thenReturn(new KnotSpeedWithBearingImpl(10, boatToBearing)); - when(trackedRace.determineBeatAngleForChart(Matchers.eq(competitor), (TimePoint) anyObject())).thenCallRealMethod(); - when(trackedRace.getBeatAngleFor(Matchers.eq(competitor), (TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); + when(trackedRace.getTWA(Matchers.eq(competitor), (TimePoint) anyObject())).thenCallRealMethod(); + when(trackedRace.getTWA(Matchers.eq(competitor), (TimePoint) anyObject(), (WindLegTypeAndLegBearingCache) anyObject())).thenCallRealMethod(); } @Test - public void testBeatAngleUpwindPort() throws NoWindException { + public void testTWAUpwindPort() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(123); final Bearing boatToBearing = new DegreeBearingImpl(348); final Bearing legBearing = new DegreeBearingImpl(303); setUp(windToBearing, boatToBearing, legBearing, LegType.UPWIND); - assertEquals(-45., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-45., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test - public void testBeatAngleUpwindStarboard() throws NoWindException { + public void testTWAUpwindStarboard() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(123); final Bearing boatToBearing = new DegreeBearingImpl(258); final Bearing legBearing = new DegreeBearingImpl(303); setUp(windToBearing, boatToBearing, legBearing, LegType.UPWIND); - assertEquals(45., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(45., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test - public void testBeatAngleDownwindPort() throws NoWindException { + public void testTWADownwindPort() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(123); final Bearing boatToBearing = new DegreeBearingImpl(120); final Bearing legBearing = new DegreeBearingImpl(123); setUp(windToBearing, boatToBearing, legBearing, LegType.DOWNWIND); - assertEquals(-177., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-177., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test - public void testBeatAngleDownwindStarboard() throws NoWindException { + public void testTWADownwindStarboard() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(123); final Bearing boatToBearing = new DegreeBearingImpl(126); final Bearing legBearing = new DegreeBearingImpl(123); setUp(windToBearing, boatToBearing, legBearing, LegType.DOWNWIND); - assertEquals(177., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(177., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test - public void testBeatAngleReachingPort() throws NoWindException { + public void testTWAReachingPort() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(123); final Bearing boatToBearing = new DegreeBearingImpl(33); final Bearing legBearing = new DegreeBearingImpl(33); setUp(windToBearing, boatToBearing, legBearing, LegType.REACHING); - assertEquals(-90., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(-90., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } @Test - public void testBeatAngleReachingStarboard() throws NoWindException { + public void testTWAReachingStarboard() throws NoWindException { final Bearing windToBearing = new DegreeBearingImpl(303); final Bearing boatToBearing = new DegreeBearingImpl(33); final Bearing legBearing = new DegreeBearingImpl(33); setUp(windToBearing, boatToBearing, legBearing, LegType.REACHING); - assertEquals(90., trackedRace.determineBeatAngleForChart(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); + assertEquals(90., trackedRace.getTWA(competitor, MillisecondsTimePoint.now()).getDegrees(), 0.00001); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index d6069406567..cc6d89407f5 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -1081,26 +1081,26 @@ public interface TrackedRace extends Serializable, IsManagedByCache sogTrack = this.getTrack(competitor); if (sogTrack != null) { SpeedWithBearing speedOverGround = sogTrack.getEstimatedSpeed(timePoint); Wind wind = cache.getWind(this, competitor, timePoint); if (wind != null && speedOverGround != null) { final Bearing projectToDirection = wind.getFrom(); - beatAngle = speedOverGround.getBearing().getDifferenceTo(projectToDirection); + twa = speedOverGround.getBearing().getDifferenceTo(projectToDirection); } } - return beatAngle; + return twa; } /** - * Same as {@link #getBeatAngle}, only that additionally a cache is provided that can allow the method to use + * Same as {@link #getTWA}, only that additionally a cache is provided that can allow the method to use * cached wind and leg type values. */ - default Bearing determineBeatAngleForChart(Competitor competitor, TimePoint at){ - return getBeatAngleFor(competitor, at, new LeaderboardDTOCalculationReuseCache(at)); + default Bearing getTWA(Competitor competitor, TimePoint at){ + return getTWA(competitor, at, new LeaderboardDTOCalculationReuseCache(at)); }; /** * Like {@link #getVelocityMadeGood(Competitor, TimePoint)}, but allowing callers to specify a cache that can diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java index 36678e03d9d..82d3c5452a0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java @@ -197,7 +197,7 @@ public class DetailTypeFormatter { case DISTANCE_TO_START_LINE: return stringMessages.distanceToLine(); case BEAT_ANGLE: - return stringMessages.beatAngle(); + return stringMessages.TWA(); case COURSE_OVER_GROUND_TRUE_DEGREES: return stringMessages.courseOverGroundTrueDegrees(); case TOTAL_SCORED_RACE_COUNT: @@ -535,7 +535,7 @@ public class DetailTypeFormatter { case RACE_TIME_TRAVELED_UPWIND: return stringMessages.raceTimeUpwindTooltip(); case BEAT_ANGLE: - return stringMessages.beatAngleTooltip(); + return stringMessages.TWATooltip(); case DISTANCE_TO_START_LINE: return ""; case COURSE_OVER_GROUND_TRUE_DEGREES: diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 273ce7fed62..af83952173b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -1090,8 +1090,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String timeToStart(String timeToStart); String timeSinceStart(String timeSince); String distanceToLine(); - String beatAngleTooltip(); - String beatAngle(); + String TWATooltip(); + String TWA(); String showBoatClassChartsLabel(); String showDiagram(); String runAutomaticallyTooltip(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 35693623dac..7eb4297c922 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1063,8 +1063,8 @@ resultFromIgtimiWindImport=The following number of fixes were imported for the l timeToStart={0} to start timeSinceStart={0} since start distanceToLine=Distance to start line -beatAngleTooltip=The angle between the competitors direction and the wind -beatAngle=True Wind Angle +TWATooltip=The angle between the competitors direction and the wind +TWA=True Wind Angle showBoatClassChartsLabel=You can also view the overall diagram for the available boat classes. showDiagram=Show Diagram runAutomaticallyTooltip=Run the query automatically, after you changed e.g. the statistic or the grouping. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 376c4b7e47a..c8c7dd45d5a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1052,8 +1052,8 @@ resultFromIgtimiWindImport=Die folgenden Anzahlen von Windmessungen wurden für timeToStart={0} bis zum Start timeSinceStart={0} seit dem Start distanceToLine=Abstand zur Startlinie -beatAngleTooltip=Der Winkel zwischen Windrichtung und Fahrtrichtung des Teilnehmers -beatAngle=Winkel zum Wind +TWATooltip=Der Winkel zwischen Windrichtung und Fahrtrichtung des Teilnehmers +TWA=Winkel zum Wind showBoatClassChartsLabel=Im folgenden können außerdem auch die existierenden Diagramme für die vorhandenen Bootsklassen angezeigt werden. showDiagram=Diagramm anzeigen runAutomaticallyTooltip=Führe die Anfrage automatisch durch, wenn zum Beispiel die Statistik oder die Gruppierung geändert wurde. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties index dfb1d686fff..af31cee058c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=Se ha importado el número siguiente de correcciones timeToStart={0} hasta la salida timeSinceStart={0} desde la salida distanceToLine=Distancia hasta la línea de salida -beatAngleTooltip=Ángulo entre la dirección de los competidores y el viento -beatAngle=Mejor ángulo +TWATooltip=Ángulo entre la dirección de los competidores y el viento +TWA=Mejor ángulo showBoatClassChartsLabel=También puede visualizar el diagrama global para las clases de embarcación disponibles. showDiagram=Mostrar diagrama runAutomaticallyTooltip=Ejecute la consulta automáticamente después de modificar, por ejemplo, la estadística o la agrupación. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index 16314e75278..b1c58ee9ebc 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=Le nombre suivant de points vent ont été importés timeToStart={0} jusqu''au départ timeSinceStart={0} depuis le départ distanceToLine=Distance jusqu''à la ligne de départ -beatAngleTooltip=L''angle entre la direction du concurrent et le vent. -beatAngle=Angle d''attaque +TWATooltip=L''angle entre la direction du concurrent et le vent. +TWA=Angle d''attaque showBoatClassChartsLabel=Vous pouvez également afficher le diagramme global pour les catégories de bateau disponibles. showDiagram=Afficher diagramme runAutomaticallyTooltip=Exécutez la requête automatiquement, par exemple après avoir modifié les statistiques ou le regroupement. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index 0339841644e..6a72bcfa1bd 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=一覧されたレースに対して次の数のフ timeToStart=スタートまで {0} timeSinceStart=スタートから {0} distanceToLine=スタートラインまでの距離 -beatAngleTooltip=競技者の方向と風の間のアングル -beatAngle=ビートアングル +TWATooltip=競技者の方向と風の間のアングル +TWA=ビートアングル showBoatClassChartsLabel=利用可能な艇種の全体図も表示することができます。 showDiagram=ダイアグラム表示 runAutomaticallyTooltip=統計やグルーピングなどを変更した後にクエリを自動的に実行します。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties index 25a3076c7f4..c3bbddfefeb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=O número seguinte de pontos fixos foi importado para timeToStart={0} até partida timeSinceStart={0} desde partida distanceToLine=Distância para linha de partida -beatAngleTooltip=O ângulo entre a direção dos competidores e o vento -beatAngle=Ângulo de cambada +TWATooltip=O ângulo entre a direção dos competidores e o vento +TWA=Ângulo de cambada showBoatClassChartsLabel=Você também pode ver o diagrama geral para as classes de barcos disponíveis. showDiagram=Visualizar diagrama runAutomaticallyTooltip=Execute a consulta automaticamente, após modificar, por exemplo, a estatística ou o agrupamento. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 6c344a82fed..92eee196c3d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=Для перечисленных гонок импо timeToStart={0} до старта timeSinceStart={0} от старта distanceToLine=Дистанция до линии старта -beatAngleTooltip=Угол между направлением участников и ветром -beatAngle=Угол удара +TWATooltip=Угол между направлением участников и ветром +TWA=Угол удара showBoatClassChartsLabel=Также можно просмотреть общую диаграмму для доступных классов лодок. showDiagram=Показать диаграмму runAutomaticallyTooltip=Автоматически выполнять запрос после изменения, например, статистики или группирования. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index a577f65b50f..6351277833b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -1058,8 +1058,8 @@ resultFromIgtimiWindImport=已为所列的比赛轮次导入以下修复: {0} timeToStart={0} - 开始 timeSinceStart={0} - 开始后 distanceToLine=与起航线的距离 -beatAngleTooltip=赛队航行的方向和风的夹角 -beatAngle=迎风航行角度 +TWATooltip=赛队航行的方向和风的夹角 +TWA=迎风航行角度 showBoatClassChartsLabel=还可以查看可用船只级别的总直直方图。 showDiagram=显示直方图 runAutomaticallyTooltip=在更改统计或分组后,自动运行查询。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ChartFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ChartFactory.java index 79ec317a5a9..e0e7074937c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ChartFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ChartFactory.java @@ -87,7 +87,7 @@ public class ChartFactory extends Chart { Chart angleChart = new Chart().setType(Type.LINE).setHeight100().setWidth100(); angleChart.setTitle(new ChartTitle().setText(""), new ChartSubtitle().setText("")); angleChart.setExporting(new Exporting().setEnabled(false)); - angleChart.getYAxis().setAxisTitleText(stringMessages.beatAngle() + " (" + stringMessages.degreesShort() + ")"); + angleChart.getYAxis().setAxisTitleText(stringMessages.TWA() + " (" + stringMessages.degreesShort() + ")"); angleChart.getXAxis().setAxisTitleText(stringMessages.windSpeed()); return angleChart; } @@ -107,7 +107,7 @@ public class ChartFactory extends Chart { lineChart.setTitle(new ChartTitle().setText(""), new ChartSubtitle().setText("")); lineChart.getXAxis().setMin(-179).setMax(180).setTickInterval(45); lineChart.getXAxis().setLabels(new XAxisLabels().setRotation(-90f).setY(30).setEnabled(true)).setAxisTitle( - new AxisTitle().setText(stringMessages.beatAngle() + " (" + stringMessages.degreesShort() + ")")); + new AxisTitle().setText(stringMessages.TWA() + " (" + stringMessages.degreesShort() + ")")); lineChart.setLegend(new Legend().setEnabled(false)); lineChart.setExporting(new Exporting().setEnabled(false)); return lineChart; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ManeuverSpeedDetailsResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ManeuverSpeedDetailsResultsPresenter.java index 030c8700952..6925b4189eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ManeuverSpeedDetailsResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ManeuverSpeedDetailsResultsPresenter.java @@ -101,7 +101,7 @@ public class ManeuverSpeedDetailsResultsPresenter extends AbstractResultsPresent }; dataCountHistogramChart = ChartFactory.createDataCountHistogramChart( - stringMessages.beatAngle() + " (" + stringMessages.degreesShort() + ")", stringMessages); + stringMessages.TWA() + " (" + stringMessages.degreesShort() + ")", stringMessages); dataCountHistogramChart.getXAxis().setMin(-179); dataCountHistogramChart.getXAxis().setMax(180); rightSideChartsWrapperPanel = new DockLayoutPanel(Unit.PCT) { @@ -138,7 +138,7 @@ public class ManeuverSpeedDetailsResultsPresenter extends AbstractResultsPresent polarChart = ChartFactory.createPolarChart(); lineChart = ChartFactory.createLineChartForPolarData(stringMessages); dataCountHistogramChart = ChartFactory.createDataCountHistogramChart( - stringMessages.beatAngle() + " (" + stringMessages.degreesShort() + ")", stringMessages); + stringMessages.TWA() + " (" + stringMessages.degreesShort() + ")", stringMessages); polarChart.getXAxis().setMin(xAxisMin).setMax(xAxisMax); lineChart.getXAxis().setMin(xAxisMin).setMax(xAxisMax); dataCountHistogramChart.getXAxis().setMin(xAxisMin).setMax(xAxisMax); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java index 31f5b5aaced..3f947da4d18 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java @@ -88,7 +88,7 @@ public class PolarResultsPresenter extends AbstractResultsPresenter { polarChartWrapperPanel.add(polarChart); dataCountHistogramChart = ChartFactory.createDataCountHistogramChart( - stringMessages.beatAngle() + " (" + stringMessages.degreesShort() + ")", stringMessages); + stringMessages.TWA() + " (" + stringMessages.degreesShort() + ")", stringMessages); dataCountPerAngleHistogramChart = ChartFactory.createDataCountHistogramChart(stringMessages.windSpeed(), stringMessages); histogramChartsWrapperPanel = new DockLayoutPanel(Unit.PCT) { diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java index 08f103dd8c4..4063bbe0ec9 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java @@ -26,7 +26,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; /** - * This container has two regressions. One for boatSpeed over windSpeed and one for beatAngle over windSpeed + * This container has two regressions. One for boatSpeed over windSpeed and one for twa over windSpeed * estimations.

* * It can return speed and angle for a given windSpeed and should only be used for restricted sets of input data. There diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java index 8a39c96c0b2..7820a9ed828 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java @@ -79,7 +79,7 @@ public class GPSFixMovingWithPolarContext implements LegTypePolarClusterKey, Ang Bearing bearing = null; TrackedLegOfCompetitor currentLeg = race.getCurrentLeg(competitor, fix.getTimePoint()); if (currentLeg != null) { - Bearing realBearing = race.determineBeatAngleForChart(competitor, fix.getTimePoint()); + Bearing realBearing = race.getTWA(competitor, fix.getTimePoint()); bearing = realBearing == null ? null : new DegreeBearingImpl(Math.abs(realBearing.getDegrees())); } WindWithConfidence> wind = race.getWindWithConfidence(fix.getPosition(), From c6f70cb6aafc54d50e2b2916dd9badde7eafe62d Mon Sep 17 00:00:00 2001 From: Kai Boernert Date: Mon, 26 Feb 2018 13:29:23 +0100 Subject: [PATCH 190/214] renamed to twa --- .../com/sap/sailing/gwt/ui/server/SailingServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 8d4a9d87fba..245ae1f9fae 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3019,8 +3019,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } break; case BEAT_ANGLE: - Bearing beatAngle = trackedRace.getBeatAngleFor(competitor, timePoint, cache); - result = beatAngle == null? null:beatAngle.getDegrees(); + Bearing twa = trackedRace.getTWA(competitor, timePoint, cache); + result = twa == null? null:twa.getDegrees(); break; case CURRENT_HEEL_IN_DEGREES: { result = getBravoBearingInDegrees(BravoFixTrack::getHeel, trackedRace, competitor, timePoint); From 407a59a61dba3cf963501572280086ec825047ac Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Feb 2018 15:50:32 +0100 Subject: [PATCH 191/214] enhanced server start-up mail; preserve mail.properties during release install Change-Id: I72d4aff7f46e3192846f780e6e98545d277edb65 --- java/target/refreshInstance.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/java/target/refreshInstance.sh b/java/target/refreshInstance.sh index fc71af629cf..72c91465afc 100755 --- a/java/target/refreshInstance.sh +++ b/java/target/refreshInstance.sh @@ -143,15 +143,17 @@ install_environment () load_from_release_file () { if [[ $INSTALL_FROM_RELEASE != "" ]]; then - echo "Build/Deployment process has been started - it can take 5 to 20 minutes until your instance is ready. " | mail -r simon.marcel.pamies@sap.com -s "Build or Deployment of $INSTANCE_ID starting" $BUILD_COMPLETE_NOTIFY + echo "Build/Deployment process has been started - it can take 5 to 20 minutes until your instance is ready. " | mail -r simon.marcel.pamies@sap.com -s "Build or Deployment of $INSTANCE_ID to $SERVER_HOME for server $SERVER_NAME starting" $BUILD_COMPLETE_NOTIFY cd $SERVER_HOME rm -f $SERVER_HOME/$INSTALL_FROM_RELEASE.tar.gz* rm -rf plugins start stop status native-libraries org.eclipse.osgi *.tar.gz echo "Loading from release file http://releases.sapsailing.com/$INSTALL_FROM_RELEASE/$INSTALL_FROM_RELEASE.tar.gz" wget http://releases.sapsailing.com/$INSTALL_FROM_RELEASE/$INSTALL_FROM_RELEASE.tar.gz mv env.sh env.sh.preserved + mv configuration/mail.properties configuration/mail.properties.preserved tar xvzf $INSTALL_FROM_RELEASE.tar.gz mv env.sh.preserved env.sh + mv configuration/mail.properties.preserved configuration/mail.properties echo "Configuration for this server is unchanged - just binaries have been changed." else echo "The variable INSTALL_FROM_RELEASE has not been set therefore no release file will be installed!" @@ -165,8 +167,10 @@ load_from_local_release_file () rm -rf plugins start stop status native-libraries org.eclipse.osgi echo "Loading from release file $INSTALL_FROM_RELEASE" mv env.sh env.sh.preserved + mv configuration/mail.properties configuration/mail.properties.preserved tar xvzf $INSTALL_FROM_RELEASE mv env.sh.preserved env.sh + mv configuration/mail.properties.preserved configuration/mail.properties echo "Configuration for this server is unchanged - just binaries have been changed." else echo "The variable INSTALL_FROM_RELEASE has not been set therefore no release file will be installed!" From 013fddec390a6825ac88018ea39cad8651fac932 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Feb 2018 16:00:04 +0100 Subject: [PATCH 192/214] fixed German TWA translation Change-Id: I98d9d4c761f7ad123294335879b66cef1ca1079b --- .../sap/sailing/gwt/ui/client/StringMessages_de.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index c8c7dd45d5a..eca6f67acfd 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1052,8 +1052,8 @@ resultFromIgtimiWindImport=Die folgenden Anzahlen von Windmessungen wurden für timeToStart={0} bis zum Start timeSinceStart={0} seit dem Start distanceToLine=Abstand zur Startlinie -TWATooltip=Der Winkel zwischen Windrichtung und Fahrtrichtung des Teilnehmers -TWA=Winkel zum Wind +TWATooltip=Der Winkel zwischen wahrer Windrichtung und Fahrtrichtung des Teilnehmers +TWA=Winkel zum wahren Wind showBoatClassChartsLabel=Im folgenden können außerdem auch die existierenden Diagramme für die vorhandenen Bootsklassen angezeigt werden. showDiagram=Diagramm anzeigen runAutomaticallyTooltip=Führe die Anfrage automatisch durch, wenn zum Beispiel die Statistik oder die Gruppierung geändert wurde. From 5330c621b102a6be525182dcbc52509a86a4d5d1 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Feb 2018 16:00:24 +0100 Subject: [PATCH 193/214] fixed Javadoc after beat angle --> TWA rename Change-Id: I11f3821cf661ef5113dde498b2ee71263b82c118 --- .../com/sap/sailing/polars/mining/AngleAndSpeedRegression.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java index 4063bbe0ec9..2d3fa5db46f 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AngleAndSpeedRegression.java @@ -26,7 +26,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; /** - * This container has two regressions. One for boatSpeed over windSpeed and one for twa over windSpeed + * This container has two regressions. One for boatSpeed over windSpeed and one for TWA (true wind angle) over windSpeed * estimations.

* * It can return speed and angle for a given windSpeed and should only be used for restricted sets of input data. There From 088afc1c41ef535b42c143871ce54eee6877fcce Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Feb 2018 16:00:43 +0100 Subject: [PATCH 194/214] formatting conventions Change-Id: I2263454ba7adfc1a974c6e10f81b6caeec5f1700 --- .../src/com/sap/sailing/domain/tracking/TrackedRace.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index cc6d89407f5..68272f8ce4a 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -1101,7 +1101,8 @@ public interface TrackedRace extends Serializable, IsManagedByCache Date: Mon, 26 Feb 2018 16:54:46 +0100 Subject: [PATCH 195/214] added close button to igtimidialog --- .../ui/adminconsole/IgtimiAccountsPanel.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index 2fe7c0ddb15..39b8c797d6a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -175,20 +175,27 @@ public class IgtimiAccountsPanel extends FlowPanel { @Override public void onSuccess(String result) { - final Button addIgtimiUserButton = new Button(stringMessages.addIgtimiUser()+" (OAuth)"); - addIgtimiUserButton.addClickHandler(clickEvent->{ + final Button addIgtimiUserButton = new Button(stringMessages.addIgtimiUser() + " (OAuth)"); + addIgtimiUserButton.addClickHandler(clickEvent -> { Frame frame = new Frame(UriUtils.fromString(result).asString()); - frame.addLoadHandler(loadEvent->refresh(sailingService, errorReporter, stringMessages)); - frame.setPixelSize(520, 770); + frame.addLoadHandler(loadEvent -> refresh(sailingService, errorReporter, stringMessages)); + frame.setPixelSize(520, 770); final CaptionImpl caption = new CaptionImpl(); caption.setText(stringMessages.addIgtimiUser()); final DialogBox dialogBox = new DialogBox(/* autoHide */ true, /* modal */ true); + Button closeButton = new Button(stringMessages.close()); + closeButton.addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + dialogBox.hide(); + } + }); dialogBox.setText(stringMessages.addIgtimiUser()); dialogBox.setGlassEnabled(true); final VerticalPanel panel = new VerticalPanel(); dialogBox.setWidget(panel); panel.add(frame); - dialogBox.addCloseHandler(event->{ + panel.add(closeButton); + dialogBox.addCloseHandler(event -> { refresh(sailingService, errorReporter, stringMessages); }); dialogBox.center(); From 7a78d388c238a68df273135ac2de66cf23309499 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 27 Feb 2018 08:17:38 +0100 Subject: [PATCH 196/214] Bug 4468: Updated the target platform due to a changed version on the Jetty update site --- .../definitions/race-analysis-p2-remote.target | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target index 709ec5135bd..1312f3565c2 100644 --- a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target +++ b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target @@ -1,5 +1,5 @@ - + @@ -10,10 +10,6 @@ - - - - @@ -90,6 +86,10 @@ + + + + From a2188ef4b777e2feb0173ed73d6f078d09f71e94 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 27 Feb 2018 08:53:45 +0100 Subject: [PATCH 197/214] Bug 4440: Fixed settings button style after master merge --- .../sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index adf6cc1dd1d..5b8a2d09f6a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -116,7 +116,7 @@ public abstract class AbstractRaceChart extends A toolbar.addStyleName(chartsCss.toolbar()); add(toolbar); settingsButton = createSettingsButton(); - settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); + settingsButton.setStyleName(chartsCss.settingsButtonBackgroundImage()); addToolbarButton(settingsButton); initWidget(rootPanel); getElement().getStyle().setMarginRight(12, Unit.PX); From 93b56b56e2ad34224e0e5a3ea15905fef47d848d Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 27 Feb 2018 09:09:38 +0100 Subject: [PATCH 198/214] Bug 4467: disabled autoHide for the Igtimi Oauth popup --- .../sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index 39b8c797d6a..57448b3ee20 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -182,7 +182,7 @@ public class IgtimiAccountsPanel extends FlowPanel { frame.setPixelSize(520, 770); final CaptionImpl caption = new CaptionImpl(); caption.setText(stringMessages.addIgtimiUser()); - final DialogBox dialogBox = new DialogBox(/* autoHide */ true, /* modal */ true); + final DialogBox dialogBox = new DialogBox(/* autoHide */ false, /* modal */ true); Button closeButton = new Button(stringMessages.close()); closeButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { From ef8dea6bacfba82a7e520e3212071b69f57e4f53 Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Thu, 1 Mar 2018 10:37:48 +0000 Subject: [PATCH 199/214] refine test cases --- wiki/bug4232-review.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/wiki/bug4232-review.md b/wiki/bug4232-review.md index f89ff10d1ce..95d9ed32576 100644 --- a/wiki/bug4232-review.md +++ b/wiki/bug4232-review.md @@ -20,15 +20,18 @@ One device will be called **A** and the other **B**, so they have unique names w * devices open the penalty fragment * device A give Omar Air OCS, device B set DNS for Omar Air * device A closes the penalty view (without publishing) -* device B should see a red warning sign (without active publish button) after the automatic refresh +* device B should see a red warning sign after the automatic refresh +* publish should be disabled and can be activated by changing every red item in the list ### Test 2 * same as [test 1](#test-scenarios_while-starting_test-1), but this time the result should be published * same result as [test 1](#test-scenarios_while-starting_test-1). +* publish should be disabled and can be activated by opening every red item in the list ### Test 3 * same as [test 1](#test-scenarios_while-starting_test-1), but after refresh device B should close the penalty view * after refresh device A should also see the red warning sign, without the possibility to publish +* publish should be disabled and can be activated by opening every red item in the list ### Test 4 * both devices open the penalty fragment @@ -64,6 +67,7 @@ One device will be called **A** and the other **B**, so they have unique names w * both devices set the penalty to different values * device A closes the result list * device B should see a red warning sign and the data should show the values from device A +* publish should be disabled and can be activated by opening every red item in the list ### Test 10 * both devices open result list @@ -76,12 +80,20 @@ One device will be called **A** and the other **B**, so they have unique names w * device A add Omar Air and Alinghi * device B add Alinghi and Omar Air * device A closes the result list -* device B should see yellow warning sign and the order Omar Air and Alinghi +* device B should see red warning sign and the order Omar Air and Alinghi +* publish should be disabled and can be activated by opening every red item in the list ### Test 12 * both devices open result list * device A add Omar Air and Alinghi * device B add Omar Air and LR Bar * device A closes the result list -* device B should see 1. Omar Air, 2. Alinghi and 2. LR Bar - because of two second places the publish button should be disabled +* device B should see 1. Omar Air, 2. Alinghi and 2. LR Bar +* publish should be disabled and can be activated by opening every item in the list and clean the duplicate rankings +### Test 13 +* devices open the penalty fragment +* device A give Omar Air OCS, device B set DNS for Omar Air +* device A pressed the home button +* device B should see a red warning sign after the automatic refresh +* publish should be disabled and can be activated by opening every red item in the list \ No newline at end of file From a445f9a768c080c916e0d512fa0cb5bc67f5a127 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 1 Mar 2018 12:08:00 +0100 Subject: [PATCH 200/214] upgraded to TracAPI 3.9.0 Change-Id: I926114bba42ea24e8629a4f52a55d1ac8600ac10 --- .../META-INF/MANIFEST.MF | 2 +- java/com.tractrac.clientmodule/README.txt | 14 ++++++++++++++ .../lib/TracAPI-src.jar | Bin 68776 -> 69352 bytes .../com.tractrac.clientmodule/lib/TracAPI.jar | Bin 3382766 -> 3382347 bytes java/com.tractrac.clientmodule/pom.xml | 2 +- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF index 6da9ddc6be7..296a00d3632 100644 --- a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF +++ b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: TracTrac Client module Bundle-SymbolicName: com.tractrac.clientmodule -Bundle-Version: 3.8.0 +Bundle-Version: 3.9.0 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ClassPath: ., lib/TracAPI.jar diff --git a/java/com.tractrac.clientmodule/README.txt b/java/com.tractrac.clientmodule/README.txt index 0e96e2a7ae5..f8f55cb9dec 100755 --- a/java/com.tractrac.clientmodule/README.txt +++ b/java/com.tractrac.clientmodule/README.txt @@ -16,6 +16,20 @@ It contains also some files: - test.sh -> script that compiles the code in the src folder, creates the test.jar library and execute the code of the example. - Manifest.txt -> manifest used to create the test.jar file +******************************************** +************* TracAPI 3.9.0 **************** +******************************************** +This is a final version. It changes the implementation of the ISensorData interface, breaking the +backward compatibility: + +Release date: 01/03/2018 +Build number: 8fdd19ee81b3ccbfc2694963e8e398346eebde7a + + 1) Features + + - Updating the interface ISensorData (Requested by Axel Uhl and Jakob Odum, 22/11/2017) + - Support for https downloading MTBs (Requested by Axel Uhl, 07/01/2018) + ******************************************** ************* TracAPI 3.8.0 **************** ******************************************** diff --git a/java/com.tractrac.clientmodule/lib/TracAPI-src.jar b/java/com.tractrac.clientmodule/lib/TracAPI-src.jar index efb9660b8d517aa7f7505ad7b65c9cfc2c115e48..b1ec60457ed0ed8b2eece4a83a86d9ce1365e862 100755 GIT binary patch delta 6398 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 YCp5>3z2so7>5^W@S_5^5zovivA9eEx1ONa4 delta 5888 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 SE?&%*2g diff --git a/java/com.tractrac.clientmodule/lib/TracAPI.jar b/java/com.tractrac.clientmodule/lib/TracAPI.jar index 20f224c3c1b76716bbb47ad64ff689a8b8c54837..a809e3e44575f05853a5b8f3495b36f7b6148e98 100755 GIT binary patch delta 184194 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 G-2Vei?syjf delta 184697 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¨ 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.0.0-SNAPSHOT com.tractrac.clientmodule - 3.8.0 + 3.9.0 eclipse-plugin From e51dfc098838571b9b0886704b8a9659520f0ba5 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 1 Mar 2018 12:09:22 +0100 Subject: [PATCH 201/214] updated TracAPI Javadoc to 3.9.0 Change-Id: Ie9777068bf7c205b529a42e190f0e594e9725d35 --- .../javadoc/allclasses-frame.html | 4 +- .../javadoc/allclasses-noframe.html | 4 +- .../tractrac/model/lib/api/IIdentifiable.html | 4 +- .../com/tractrac/model/lib/api/INamed.html | 4 +- .../tractrac/model/lib/api/ModelLocator.html | 4 +- .../api/attachment/AbstractAttachable.html | 4 +- .../model/lib/api/attachment/IAttachable.html | 4 +- .../lib/api/attachment/IAttachmentKey.html | 4 +- .../api/attachment/IAttachmentManager.html | 4 +- .../lib/api/attachment/package-frame.html | 4 +- .../lib/api/attachment/package-summary.html | 4 +- .../lib/api/attachment/package-tree.html | 4 +- .../model/lib/api/data/IControlPassing.html | 4 +- .../model/lib/api/data/IControlPassings.html | 4 +- .../model/lib/api/data/IMessageData.html | 4 +- .../model/lib/api/data/IPosition.html | 4 +- .../model/lib/api/data/IPositionFactory.html | 4 +- .../model/lib/api/data/IPositionOffset.html | 4 +- .../model/lib/api/data/IPositionSnapped.html | 4 +- .../model/lib/api/data/IStartStopData.html | 4 +- .../model/lib/api/data/ITimeData.html | 4 +- .../model/lib/api/data/package-frame.html | 4 +- .../model/lib/api/data/package-summary.html | 4 +- .../model/lib/api/data/package-tree.html | 4 +- .../lib/api/event/CreateModelException.html | 4 +- .../model/lib/api/event/DataSource.html | 4 +- .../model/lib/api/event/EventType.html | 4 +- .../model/lib/api/event/ICompetitor.html | 4 +- .../model/lib/api/event/ICompetitorClass.html | 4 +- .../tractrac/model/lib/api/event/IEvent.html | 4 +- .../model/lib/api/event/IEventFactory.html | 4 +- .../tractrac/model/lib/api/event/IRace.html | 4 +- .../model/lib/api/event/IRaceCompetitor.html | 4 +- .../model/lib/api/event/IRaceSerie.html | 4 +- .../tractrac/model/lib/api/event/ITeam.html | 4 +- .../api/event/RaceCompetitorStatusType.html | 4 +- .../lib/api/event/RaceLoadingException.html | 4 +- .../model/lib/api/event/RaceStatusType.html | 4 +- .../lib/api/event/RaceVisibilityType.html | 4 +- .../model/lib/api/event/StartTimeType.html | 4 +- .../model/lib/api/event/package-frame.html | 4 +- .../model/lib/api/event/package-summary.html | 4 +- .../model/lib/api/event/package-tree.html | 12 +- .../model/lib/api/metadata/IMetadata.html | 4 +- .../lib/api/metadata/IMetadataContainer.html | 4 +- .../lib/api/metadata/IMetadataFactory.html | 4 +- .../api/metadata/IPropertiesContainer.html | 4 +- .../model/lib/api/metadata/package-frame.html | 4 +- .../lib/api/metadata/package-summary.html | 4 +- .../model/lib/api/metadata/package-tree.html | 4 +- .../tractrac/model/lib/api/package-frame.html | 4 +- .../model/lib/api/package-summary.html | 4 +- .../tractrac/model/lib/api/package-tree.html | 4 +- .../model/lib/api/route/IControl.html | 4 +- .../model/lib/api/route/IControlPoint.html | 4 +- .../model/lib/api/route/IControlRoute.html | 4 +- .../model/lib/api/route/IPathRoute.html | 4 +- .../lib/api/route/IPathRouteFactory.html | 4 +- .../model/lib/api/route/IPathSegment.html | 4 +- .../tractrac/model/lib/api/route/IRoute.html | 4 +- .../model/lib/api/route/ISegment.html | 4 +- .../model/lib/api/route/package-frame.html | 4 +- .../model/lib/api/route/package-summary.html | 4 +- .../model/lib/api/route/package-tree.html | 4 +- .../model/lib/api/sensor/ISensorData.html | 147 ++++++++++-------- .../model/lib/api/sensor/package-frame.html | 4 +- .../model/lib/api/sensor/package-summary.html | 7 +- .../model/lib/api/sensor/package-tree.html | 4 +- .../model/lib/api/spatial/ICoordinate.html | 4 +- .../lib/api/spatial/ICoordinateSequence.html | 4 +- .../model/lib/api/spatial/IExtent.html | 4 +- .../model/lib/api/spatial/IGeoCoordinate.html | 4 +- .../model/lib/api/spatial/package-frame.html | 4 +- .../lib/api/spatial/package-summary.html | 4 +- .../model/lib/api/spatial/package-tree.html | 4 +- .../lib/api/IEventSubscriber.html | 4 +- .../subscription/lib/api/IRaceSubscriber.html | 4 +- .../subscription/lib/api/ISubscriber.html | 4 +- .../lib/api/ISubscriberFactory.html | 4 +- .../lib/api/ISubscriberListener.html | 4 +- .../SubscriberInitializationException.html | 4 +- .../lib/api/SubscriptionLocator.html | 4 +- .../ICompetitorSensorDataListener.html | 4 +- .../api/competitor/ICompetitorsListener.html | 4 +- .../lib/api/competitor/IPositionListener.html | 4 +- .../competitor/IPositionOffsetListener.html | 4 +- .../competitor/IPositionSnappedListener.html | 4 +- .../lib/api/competitor/package-frame.html | 4 +- .../lib/api/competitor/package-summary.html | 4 +- .../lib/api/competitor/package-tree.html | 4 +- .../api/control/IControlPassingsListener.html | 4 +- .../IControlPointPositionListener.html | 4 +- .../IControlPointSensorDataListener.html | 4 +- .../control/IControlRouteChangeListener.html | 4 +- .../lib/api/control/IControlsListener.html | 4 +- .../lib/api/control/package-frame.html | 4 +- .../lib/api/control/package-summary.html | 4 +- .../lib/api/control/package-tree.html | 4 +- .../api/event/IConnectionStatusListener.html | 4 +- .../lib/api/event/IEventMessageListener.html | 4 +- .../api/event/ILiveDataEvent.StatusType.html | 4 +- .../lib/api/event/ILiveDataEvent.html | 4 +- .../subscription/lib/api/event/IMessage.html | 4 +- .../lib/api/event/IServerTimeListener.html | 4 +- .../lib/api/event/IStoredDataEvent.Type.html | 4 +- .../lib/api/event/IStoredDataEvent.html | 4 +- .../lib/api/event/package-frame.html | 4 +- .../lib/api/event/package-summary.html | 4 +- .../lib/api/event/package-tree.html | 6 +- .../subscription/lib/api/package-frame.html | 4 +- .../subscription/lib/api/package-summary.html | 4 +- .../subscription/lib/api/package-tree.html | 4 +- .../lib/api/race/IRaceCompetitorListener.html | 4 +- .../lib/api/race/IRaceMessageListener.html | 4 +- .../IRaceStartStopTimesChangeListener.html | 4 +- .../lib/api/race/IRacesListener.html | 4 +- .../race/IStartStopTimesChangeListener.html | 4 +- .../lib/api/race/package-frame.html | 4 +- .../lib/api/race/package-summary.html | 4 +- .../lib/api/race/package-tree.html | 4 +- .../lib/api/route/IRoutesListener.html | 4 +- .../lib/api/route/package-frame.html | 4 +- .../lib/api/route/package-summary.html | 4 +- .../lib/api/route/package-tree.html | 4 +- .../javadoc/constant-values.html | 4 +- .../javadoc/deprecated-list.html | 4 +- .../javadoc/help-doc.html | 4 +- .../javadoc/index-files/index-1.html | 4 +- .../javadoc/index-files/index-10.html | 4 +- .../javadoc/index-files/index-11.html | 4 +- .../javadoc/index-files/index-12.html | 4 +- .../javadoc/index-files/index-13.html | 4 +- .../javadoc/index-files/index-14.html | 4 +- .../javadoc/index-files/index-15.html | 4 +- .../javadoc/index-files/index-16.html | 4 +- .../javadoc/index-files/index-17.html | 4 +- .../javadoc/index-files/index-18.html | 4 +- .../javadoc/index-files/index-2.html | 4 +- .../javadoc/index-files/index-3.html | 4 +- .../javadoc/index-files/index-4.html | 4 +- .../javadoc/index-files/index-5.html | 4 +- .../javadoc/index-files/index-6.html | 4 +- .../javadoc/index-files/index-7.html | 31 ++-- .../javadoc/index-files/index-8.html | 4 +- .../javadoc/index-files/index-9.html | 7 +- .../javadoc/index.html | 2 +- .../javadoc/overview-frame.html | 4 +- .../javadoc/overview-summary.html | 4 +- .../javadoc/overview-tree.html | 4 +- .../javadoc/serialized-form.html | 4 +- 150 files changed, 405 insertions(+), 379 deletions(-) diff --git a/java/com.tractrac.clientmodule/javadoc/allclasses-frame.html b/java/com.tractrac.clientmodule/javadoc/allclasses-frame.html index 859c398f090..f86e37658c5 100755 --- a/java/com.tractrac.clientmodule/javadoc/allclasses-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/allclasses-frame.html @@ -2,9 +2,9 @@ - + All Classes - + diff --git a/java/com.tractrac.clientmodule/javadoc/allclasses-noframe.html b/java/com.tractrac.clientmodule/javadoc/allclasses-noframe.html index a52a3bedec2..5a8ce247fcc 100755 --- a/java/com.tractrac.clientmodule/javadoc/allclasses-noframe.html +++ b/java/com.tractrac.clientmodule/javadoc/allclasses-noframe.html @@ -2,9 +2,9 @@ - + All Classes - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/IIdentifiable.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/IIdentifiable.html index 1bfd1664530..f49a16cacac 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/IIdentifiable.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/IIdentifiable.html @@ -2,9 +2,9 @@ - + IIdentifiable - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/INamed.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/INamed.html index 2d0d21559cc..fb9dd2a79c9 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/INamed.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/INamed.html @@ -2,9 +2,9 @@ - + INamed - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/ModelLocator.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/ModelLocator.html index ecdde91d5fd..591593cb611 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/ModelLocator.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/ModelLocator.html @@ -2,9 +2,9 @@ - + ModelLocator - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/AbstractAttachable.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/AbstractAttachable.html index 082d35cc400..64d77a2065e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/AbstractAttachable.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/AbstractAttachable.html @@ -2,9 +2,9 @@ - + AbstractAttachable - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachable.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachable.html index 5be0214ab17..59a28976bd4 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachable.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachable.html @@ -2,9 +2,9 @@ - + IAttachable - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentKey.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentKey.html index 7a3a54301a0..0567b60dcb3 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentKey.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentKey.html @@ -2,9 +2,9 @@ - + IAttachmentKey - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentManager.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentManager.html index 8568fc5136c..3ab58d1f5ba 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentManager.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/IAttachmentManager.html @@ -2,9 +2,9 @@ - + IAttachmentManager - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-frame.html index 3ea356a1f99..87cdc2d5b6d 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.attachment - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-summary.html index 967681fb8a8..1d5f3965136 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.attachment - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-tree.html index a365d757722..87eb6354fba 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/attachment/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.attachment Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassing.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassing.html index 8f446d4ff4d..d087e140465 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassing.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassing.html @@ -2,9 +2,9 @@ - + IControlPassing - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassings.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassings.html index 4983120c653..743da0b05cb 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassings.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IControlPassings.html @@ -2,9 +2,9 @@ - + IControlPassings - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IMessageData.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IMessageData.html index fbdd5398e54..9f277483f52 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IMessageData.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IMessageData.html @@ -2,9 +2,9 @@ - + IMessageData - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPosition.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPosition.html index 1bb267c2cac..9ddb38d0dfa 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPosition.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPosition.html @@ -2,9 +2,9 @@ - + IPosition - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionFactory.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionFactory.html index 580b7a20b43..8f70a6a6c18 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionFactory.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionFactory.html @@ -2,9 +2,9 @@ - + IPositionFactory - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionOffset.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionOffset.html index 94802caad10..94489d0acd3 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionOffset.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionOffset.html @@ -2,9 +2,9 @@ - + IPositionOffset - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionSnapped.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionSnapped.html index 1074a0c4cc6..4037cecf70a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionSnapped.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IPositionSnapped.html @@ -2,9 +2,9 @@ - + IPositionSnapped - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IStartStopData.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IStartStopData.html index e01ad8709d9..e8e6a19a88e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IStartStopData.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/IStartStopData.html @@ -2,9 +2,9 @@ - + IStartStopData - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/ITimeData.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/ITimeData.html index 35dee7d2332..2c1fa868748 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/ITimeData.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/ITimeData.html @@ -2,9 +2,9 @@ - + ITimeData - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-frame.html index e140cc41722..024e31ebe3e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.data - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-summary.html index 453e6df06d5..51a2b776969 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.data - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-tree.html index 4b2a9dddcb3..c11aa91f671 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/data/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.data Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/CreateModelException.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/CreateModelException.html index 5dd7c3aaea0..b8754c6480f 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/CreateModelException.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/CreateModelException.html @@ -2,9 +2,9 @@ - + CreateModelException - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/DataSource.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/DataSource.html index f54a2d257dd..ed9a9454e89 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/DataSource.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/DataSource.html @@ -2,9 +2,9 @@ - + DataSource - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/EventType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/EventType.html index c7f4adb6704..329367f9f2a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/EventType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/EventType.html @@ -2,9 +2,9 @@ - + EventType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitor.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitor.html index 02c4cc8da35..01385028c91 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitor.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitor.html @@ -2,9 +2,9 @@ - + ICompetitor - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitorClass.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitorClass.html index 6e30f501972..90955e28549 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitorClass.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ICompetitorClass.html @@ -2,9 +2,9 @@ - + ICompetitorClass - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEvent.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEvent.html index 0eabdc2b51b..1c2668de721 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEvent.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEvent.html @@ -2,9 +2,9 @@ - + IEvent - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEventFactory.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEventFactory.html index 7734d56364c..24aba985eb0 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEventFactory.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IEventFactory.html @@ -2,9 +2,9 @@ - + IEventFactory - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRace.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRace.html index a3b5e48ac69..1c2ed2eba93 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRace.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRace.html @@ -2,9 +2,9 @@ - + IRace - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceCompetitor.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceCompetitor.html index f4c99b5036b..cd1fb1de69f 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceCompetitor.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceCompetitor.html @@ -2,9 +2,9 @@ - + IRaceCompetitor - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceSerie.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceSerie.html index c30b131bb15..d493924d72c 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceSerie.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/IRaceSerie.html @@ -2,9 +2,9 @@ - + IRaceSerie - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ITeam.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ITeam.html index 874cc3bf9a1..47f83312e70 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ITeam.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/ITeam.html @@ -2,9 +2,9 @@ - + ITeam - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceCompetitorStatusType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceCompetitorStatusType.html index c22ce1c8c1a..dae15f7441f 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceCompetitorStatusType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceCompetitorStatusType.html @@ -2,9 +2,9 @@ - + RaceCompetitorStatusType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceLoadingException.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceLoadingException.html index 62c1c408d5e..5d383a1f7de 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceLoadingException.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceLoadingException.html @@ -2,9 +2,9 @@ - + RaceLoadingException - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceStatusType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceStatusType.html index b83e8d1c6c1..013f4b81d28 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceStatusType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceStatusType.html @@ -2,9 +2,9 @@ - + RaceStatusType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceVisibilityType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceVisibilityType.html index a106c91d71c..6ed73b8f6cd 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceVisibilityType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/RaceVisibilityType.html @@ -2,9 +2,9 @@ - + RaceVisibilityType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/StartTimeType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/StartTimeType.html index 52ffa04b064..3d2569fd89e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/StartTimeType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/StartTimeType.html @@ -2,9 +2,9 @@ - + StartTimeType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-frame.html index 2217386c41f..232e8b1b7d0 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.event - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-summary.html index 7e0836b21a0..ee629209e51 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.event - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-tree.html index fe396165ad9..89a04e5f102 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/event/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.event Class Hierarchy - + @@ -150,12 +150,12 @@

diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadata.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadata.html index b0ab7dcee7c..673f8952a35 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadata.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadata.html @@ -2,9 +2,9 @@ - + IMetadata - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataContainer.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataContainer.html index 0e67f95567c..bc3e20c079c 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataContainer.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataContainer.html @@ -2,9 +2,9 @@ - + IMetadataContainer - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataFactory.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataFactory.html index ee036635fea..da195961a97 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataFactory.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IMetadataFactory.html @@ -2,9 +2,9 @@ - + IMetadataFactory - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IPropertiesContainer.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IPropertiesContainer.html index 4f45229c90d..c910cc2c7fd 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IPropertiesContainer.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/IPropertiesContainer.html @@ -2,9 +2,9 @@ - + IPropertiesContainer - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-frame.html index 93b05901bed..2d8b1a9d0e7 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.metadata - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-summary.html index e2037213857..c801803ec5e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.metadata - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-tree.html index 4d7bc684038..73b26f6102b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/metadata/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.metadata Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-frame.html index 0fe045d687a..23a97244b97 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-summary.html index 7e61e5ca263..59acde8d050 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-tree.html index 7fdce4a1148..35294545caf 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControl.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControl.html index 11639c54d7d..f5477d4cbb9 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControl.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControl.html @@ -2,9 +2,9 @@ - + IControl - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlPoint.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlPoint.html index 948d8cd63c3..e594d564788 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlPoint.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlPoint.html @@ -2,9 +2,9 @@ - + IControlPoint - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlRoute.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlRoute.html index b1b60e038ec..188dae6cb70 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlRoute.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IControlRoute.html @@ -2,9 +2,9 @@ - + IControlRoute - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRoute.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRoute.html index a2585529129..718d45afcda 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRoute.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRoute.html @@ -2,9 +2,9 @@ - + IPathRoute - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRouteFactory.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRouteFactory.html index 2a3a858979e..17c1e9c7e05 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRouteFactory.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathRouteFactory.html @@ -2,9 +2,9 @@ - + IPathRouteFactory - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathSegment.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathSegment.html index 5b170df9510..43c68e074a8 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathSegment.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IPathSegment.html @@ -2,9 +2,9 @@ - + IPathSegment - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IRoute.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IRoute.html index 55e922421ef..d99de066009 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IRoute.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/IRoute.html @@ -2,9 +2,9 @@ - + IRoute - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/ISegment.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/ISegment.html index 04805ffedfa..41a1908b62c 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/ISegment.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/ISegment.html @@ -2,9 +2,9 @@ - + ISegment - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-frame.html index 045161dfb4b..1fee5de1fc9 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.route - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-summary.html index e79c94e6205..abb67c780d0 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.route - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-tree.html index 262ea87dcb5..d27f426b0ca 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/route/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.route Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/ISensorData.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/ISensorData.html index 1208ec8dead..2c2b577ac27 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/ISensorData.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/ISensorData.html @@ -2,9 +2,9 @@ - + ISensorData - + @@ -18,7 +18,7 @@ catch(err) { } //--> -var methods = {"i0":6,"i1":6,"i2":6,"i3":6,"i4":6,"i5":6,"i6":6}; +var methods = {"i0":6,"i1":6,"i2":6,"i3":6,"i4":6}; var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],4:["t3","Abstract Methods"]}; var altColor = "altColor"; var rowColor = "rowColor"; @@ -106,8 +106,18 @@ var activeTableTab = "activeTableTab";
public interface ISensorData
 extends ITimeData
-
This interface implements sensor data. It has some methods to retrieve custom dimensions - and it also has methods to retrieve sensor data in a raw format
+

+ This interface implements sensor data. It has methods to retrieve all the dimensions + that are supported at this moment (at it will be extended with new methods when the + system supports new types of sensors). All the dimensions have the same time stamp + (returned by the @ITimeData.getTimestamp() method). +

+

+ If the method for a dimension returns null, means that there is not a value + for this dimension at this time (it can arrive in a future message). The library doesn't + interpolate values for the dimensions and only returns values that have been retrieved + from the physical sensor. +

@@ -129,43 +139,40 @@ extends java.lang.String getDimensionAt(int index) -
Gets a dimension by index
+
+ Gets a dimension by index (it can be used if the consumer app knows the + order of the dimensions.
-float +java.lang.Float getHeel() -
Gets the heel
+
+ Gets the heel, defined as the tilting rotation of a vessel about its + longitudinal/X (front-back or bow-stern) axis.
java.lang.String getRaw() -
Gets the sensor data in a raw format.
+
+ Gets the sensor data in a raw format.
-float +java.lang.Float getRideHeight() -
Gets the ride height
+
+ Gets the ride height, defined as the distance between the base of the mast + and the water.
-float +java.lang.Float getTrim() -
Gets the trim
- - - -float -getVMG() -
Gets the VMG
- - - -float -getWind() -
Gets the wind
+
+ Gets the trim, defined as the up/down rotation of a vessel about its + lateral/Y (side-to-side or port-starboard) axis.
@@ -196,8 +203,20 @@ extends
  • getHeel

    -
    float getHeel()
    -
    Gets the heel
    +
    java.lang.Float getHeel()
    +

    + Gets the heel, defined as the tilting rotation of a vessel about its + longitudinal/X (front-back or bow-stern) axis. +

    +

    + It's measured in degrees, and it's positive when the boat is on + starboard tack. 0 means that the boat is not heeled +

    +

    + null means that there is not a measure for this + dimension at this time (but a correct value can arrive in a + future message) +

    Returns:
    the heel
    @@ -210,8 +229,20 @@ extends
  • getTrim

    -
    float getTrim()
    -
    Gets the trim
    +
    java.lang.Float getTrim()
    +

    + Gets the trim, defined as the up/down rotation of a vessel about its + lateral/Y (side-to-side or port-starboard) axis. +

    +

    + It is measured in degrees, and it's positive when the bow is up. + 0 means that the boat is not trimmed +

    +

    + null means that there is not a measure for this + dimension at this time (but a correct value can arrive in a + future message) +

    Returns:
    the trim
    @@ -224,42 +255,25 @@ extends
  • getRideHeight

    -
    float getRideHeight()
    -
    Gets the ride height
    +
    java.lang.Float getRideHeight()
    +

    + Gets the ride height, defined as the distance between the base of the mast + and the water. +

    +

    + It is measured in meters. +

    +

    + null means that there is not a measure for this + dimension at this time (but a correct value can arrive in a + future message) +

    Returns:
    the ride height
  • -
    - - -
      -
    • -

      getWind

      -
      float getWind()
      -
      Gets the wind
      -
      -
      Returns:
      -
      the wind
      -
      -
    • -
    - - - -
      -
    • -

      getVMG

      -
      float getVMG()
      -
      Gets the VMG
      -
      -
      Returns:
      -
      the VMG
      -
      -
    • -
    @@ -267,8 +281,10 @@ extends

    getRaw

    java.lang.String getRaw()
    -
    Gets the sensor data in a raw format. It is just an String with - all the dimensions separated by ";"
    +

    + Gets the sensor data in a raw format. It is just an String with + all the dimensions separated by ";" +

    Returns:
    all sensor data in a raw format
    @@ -282,7 +298,14 @@ extends

    getDimensionAt

    java.lang.String getDimensionAt(int index)
    -
    Gets a dimension by index
    +

    + Gets a dimension by index (it can be used if the consumer app knows the + order of the dimensions. +

    +

    + It returns null if the dimension doesn't has a valid value + at this timestamp +

    Parameters:
    index - the index of the dimension
    diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-frame.html index 489340764be..a255db40e3a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.sensor - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-summary.html index dfdc978d316..bd9d4c3021a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.sensor - + @@ -84,7 +84,8 @@
    ISensorData -
    This interface implements sensor data.
    +
    + This interface implements sensor data.
    diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-tree.html index c9f446060ac..03f95a148a2 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/sensor/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.sensor Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinate.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinate.html index 311ad53aaa5..26ea763bc1b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinate.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinate.html @@ -2,9 +2,9 @@ - + ICoordinate - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinateSequence.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinateSequence.html index 3e903b2af86..d4200a4863d 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinateSequence.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/ICoordinateSequence.html @@ -2,9 +2,9 @@ - + ICoordinateSequence - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IExtent.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IExtent.html index a93a07c439f..aac0fea8509 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IExtent.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IExtent.html @@ -2,9 +2,9 @@ - + IExtent - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IGeoCoordinate.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IGeoCoordinate.html index 72bf71b24dc..dd9e55e0833 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IGeoCoordinate.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/IGeoCoordinate.html @@ -2,9 +2,9 @@ - + IGeoCoordinate - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-frame.html index e3c14e3b127..a7aacb16202 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.spatial - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-summary.html index a864314221f..14b2c752ac3 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.spatial - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-tree.html index 3c2aa91c6d9..c0a5d1f3210 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/model/lib/api/spatial/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.model.lib.api.spatial Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IEventSubscriber.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IEventSubscriber.html index f50a638016b..68e6367443a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IEventSubscriber.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IEventSubscriber.html @@ -2,9 +2,9 @@ - + IEventSubscriber - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IRaceSubscriber.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IRaceSubscriber.html index 9c70f977881..10bbb6329a7 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IRaceSubscriber.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/IRaceSubscriber.html @@ -2,9 +2,9 @@ - + IRaceSubscriber - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriber.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriber.html index 72c3ab44d7c..e0f71920439 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriber.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriber.html @@ -2,9 +2,9 @@ - + ISubscriber - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberFactory.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberFactory.html index 6ce6d90cfc4..2d3cba3af95 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberFactory.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberFactory.html @@ -2,9 +2,9 @@ - + ISubscriberFactory - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberListener.html index 4aceaf07445..5f834860b53 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/ISubscriberListener.html @@ -2,9 +2,9 @@ - + ISubscriberListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriberInitializationException.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriberInitializationException.html index d272f7ba7b3..0a24d64a995 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriberInitializationException.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriberInitializationException.html @@ -2,9 +2,9 @@ - + SubscriberInitializationException - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriptionLocator.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriptionLocator.html index 68e1852d2e2..e253959ecaa 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriptionLocator.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/SubscriptionLocator.html @@ -2,9 +2,9 @@ - + SubscriptionLocator - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorSensorDataListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorSensorDataListener.html index 6d77a09a4d6..0f5ef04c7e2 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorSensorDataListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorSensorDataListener.html @@ -2,9 +2,9 @@ - + ICompetitorSensorDataListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorsListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorsListener.html index f26df2c8168..7e23882c93a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorsListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/ICompetitorsListener.html @@ -2,9 +2,9 @@ - + ICompetitorsListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionListener.html index fdd09a61d7e..0c7761b386b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionListener.html @@ -2,9 +2,9 @@ - + IPositionListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionOffsetListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionOffsetListener.html index 6633a869b4f..df89f8f44fe 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionOffsetListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionOffsetListener.html @@ -2,9 +2,9 @@ - + IPositionOffsetListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionSnappedListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionSnappedListener.html index 072b6d3ca21..91cf5c4be14 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionSnappedListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/IPositionSnappedListener.html @@ -2,9 +2,9 @@ - + IPositionSnappedListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-frame.html index 161e06dae6d..40a5448ef7a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.competitor - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-summary.html index 62d73939dea..45c72ecc9c1 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.competitor - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-tree.html index d5879133b2a..3eb890d22b0 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/competitor/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.competitor Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPassingsListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPassingsListener.html index e8fe586d727..dfa7d07d2e2 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPassingsListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPassingsListener.html @@ -2,9 +2,9 @@ - + IControlPassingsListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointPositionListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointPositionListener.html index ec9789f5cef..fd258d76c3b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointPositionListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointPositionListener.html @@ -2,9 +2,9 @@ - + IControlPointPositionListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointSensorDataListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointSensorDataListener.html index 602806a4f2a..c8a2e1c50e3 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointSensorDataListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlPointSensorDataListener.html @@ -2,9 +2,9 @@ - + IControlPointSensorDataListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlRouteChangeListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlRouteChangeListener.html index 2d2679e5b66..5c67f23ad49 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlRouteChangeListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlRouteChangeListener.html @@ -2,9 +2,9 @@ - + IControlRouteChangeListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlsListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlsListener.html index 8c2cdb26ada..be9f1fb530a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlsListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/IControlsListener.html @@ -2,9 +2,9 @@ - + IControlsListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-frame.html index abf8a19fb6b..86f4773f2ad 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.control - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-summary.html index 90a214adc10..5d11ac74f8b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.control - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-tree.html index b14995df5ff..664a672a01e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/control/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.control Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IConnectionStatusListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IConnectionStatusListener.html index 5968e5a8d33..26868e2f4b1 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IConnectionStatusListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IConnectionStatusListener.html @@ -2,9 +2,9 @@ - + IConnectionStatusListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IEventMessageListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IEventMessageListener.html index 1f51db0aac8..5e439fd3468 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IEventMessageListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IEventMessageListener.html @@ -2,9 +2,9 @@ - + IEventMessageListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.StatusType.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.StatusType.html index 219cc6aa7ce..21106cd7250 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.StatusType.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.StatusType.html @@ -2,9 +2,9 @@ - + ILiveDataEvent.StatusType - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.html index cedae74b981..75aa842f285 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/ILiveDataEvent.html @@ -2,9 +2,9 @@ - + ILiveDataEvent - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IMessage.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IMessage.html index 627439a79c4..898a5e03f9d 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IMessage.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IMessage.html @@ -2,9 +2,9 @@ - + IMessage - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IServerTimeListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IServerTimeListener.html index bc1c48007a8..4b24f68ca32 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IServerTimeListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IServerTimeListener.html @@ -2,9 +2,9 @@ - + IServerTimeListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.Type.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.Type.html index dec51601ddb..842979e127c 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.Type.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.Type.html @@ -2,9 +2,9 @@ - + IStoredDataEvent.Type - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.html index 9106ee9fc74..2257eccb186 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/IStoredDataEvent.html @@ -2,9 +2,9 @@ - + IStoredDataEvent - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-frame.html index 3e17f82957c..7d9f1a2bdf5 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.event - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-summary.html index 8acf440b543..6ec078405d1 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.event - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-tree.html index b54dcd81713..4d9ae8d39a2 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/event/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.event Class Hierarchy - + @@ -95,8 +95,8 @@ diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-frame.html index 6c39de14161..337ee36410a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-summary.html index 1000254e8a9..d4dc2abf09e 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-tree.html index 01f681326df..783dda8e08a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceCompetitorListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceCompetitorListener.html index ac032d3b9bd..54798e05a39 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceCompetitorListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceCompetitorListener.html @@ -2,9 +2,9 @@ - + IRaceCompetitorListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceMessageListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceMessageListener.html index 6548150dfe8..b8c848e7603 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceMessageListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceMessageListener.html @@ -2,9 +2,9 @@ - + IRaceMessageListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceStartStopTimesChangeListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceStartStopTimesChangeListener.html index 36fa9beac2a..ac2ff8c875f 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceStartStopTimesChangeListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRaceStartStopTimesChangeListener.html @@ -2,9 +2,9 @@ - + IRaceStartStopTimesChangeListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRacesListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRacesListener.html index bd3aefb5ad4..2bbb0d8ff0f 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRacesListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IRacesListener.html @@ -2,9 +2,9 @@ - + IRacesListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IStartStopTimesChangeListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IStartStopTimesChangeListener.html index 7080677521c..c7d00deb5f0 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IStartStopTimesChangeListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/IStartStopTimesChangeListener.html @@ -2,9 +2,9 @@ - + IStartStopTimesChangeListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-frame.html index 6729c3e1674..2510a6e4af8 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.race - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-summary.html index 2c002c8b660..13bdb7fcfbb 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.race - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-tree.html index 6af70423a88..b3a8754259c 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/race/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.race Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/IRoutesListener.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/IRoutesListener.html index 16f74892b4c..f965f5b036a 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/IRoutesListener.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/IRoutesListener.html @@ -2,9 +2,9 @@ - + IRoutesListener - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-frame.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-frame.html index 37586170c42..60d6428454b 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-frame.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-frame.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.route - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-summary.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-summary.html index 96bfe893a6f..98dddd0f1e1 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-summary.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.route - + diff --git a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-tree.html b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-tree.html index cd9b54867b4..54da9f1efd7 100755 --- a/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/com/tractrac/subscription/lib/api/route/package-tree.html @@ -2,9 +2,9 @@ - + com.tractrac.subscription.lib.api.route Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/constant-values.html b/java/com.tractrac.clientmodule/javadoc/constant-values.html index a15c886d233..7e53af581ac 100755 --- a/java/com.tractrac.clientmodule/javadoc/constant-values.html +++ b/java/com.tractrac.clientmodule/javadoc/constant-values.html @@ -2,9 +2,9 @@ - + Constant Field Values - + diff --git a/java/com.tractrac.clientmodule/javadoc/deprecated-list.html b/java/com.tractrac.clientmodule/javadoc/deprecated-list.html index 7a7ca673520..64a036e0cc4 100755 --- a/java/com.tractrac.clientmodule/javadoc/deprecated-list.html +++ b/java/com.tractrac.clientmodule/javadoc/deprecated-list.html @@ -2,9 +2,9 @@ - + Deprecated List - + diff --git a/java/com.tractrac.clientmodule/javadoc/help-doc.html b/java/com.tractrac.clientmodule/javadoc/help-doc.html index 4e7c1d32b41..fbbf59e5a73 100755 --- a/java/com.tractrac.clientmodule/javadoc/help-doc.html +++ b/java/com.tractrac.clientmodule/javadoc/help-doc.html @@ -2,9 +2,9 @@ - + API Help - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-1.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-1.html index 0d20c0f1dc3..396e6e7520a 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-1.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-1.html @@ -2,9 +2,9 @@ - + A-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-10.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-10.html index 0d51e37fa8d..92204ffcf76 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-10.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-10.html @@ -2,9 +2,9 @@ - + L-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-11.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-11.html index abbfbb9106e..b9de578e906 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-11.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-11.html @@ -2,9 +2,9 @@ - + M-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-12.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-12.html index a5812f80029..e24a3b345b6 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-12.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-12.html @@ -2,9 +2,9 @@ - + N-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-13.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-13.html index 4dc051721b7..cd51bab8c00 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-13.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-13.html @@ -2,9 +2,9 @@ - + O-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-14.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-14.html index 62e137086ce..f2b6234d5d4 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-14.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-14.html @@ -2,9 +2,9 @@ - + R-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-15.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-15.html index d45ad461c32..0deb5bd0b9e 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-15.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-15.html @@ -2,9 +2,9 @@ - + S-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-16.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-16.html index a5b17c63534..92a4ffdf77d 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-16.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-16.html @@ -2,9 +2,9 @@ - + T-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-17.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-17.html index c04800eeca4..7097180c455 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-17.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-17.html @@ -2,9 +2,9 @@ - + U-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-18.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-18.html index de3aa7f61f0..eeb86b1559a 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-18.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-18.html @@ -2,9 +2,9 @@ - + V-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-2.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-2.html index 4d36bcfebd9..24b6ebbcb12 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-2.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-2.html @@ -2,9 +2,9 @@ - + B-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-3.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-3.html index b64c4d60bf8..648205eb66f 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-3.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-3.html @@ -2,9 +2,9 @@ - + C-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html index f5fbecbbb04..a64377a8920 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html @@ -2,9 +2,9 @@ - + D-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html index f0ed8454ed7..c580af62957 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html @@ -2,9 +2,9 @@ - + E-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html index c34797543b9..a2ab5a62f09 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html @@ -2,9 +2,9 @@ - + F-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html index dce0bb7561f..5ca7ad07f28 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html @@ -2,9 +2,9 @@ - + G-Index - + @@ -197,7 +197,9 @@
    getDimensionAt(int) - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    Gets a dimension by index
    +
    + Gets a dimension by index (it can be used if the consumer app knows the + order of the dimensions.
    getDirection() - Method in interface com.tractrac.model.lib.api.data.IPosition
    @@ -272,7 +274,9 @@
    getHeel() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    Gets the heel
    +
    + Gets the heel, defined as the tilting rotation of a vessel about its + longitudinal/X (front-back or bow-stern) axis.
    getHeight() - Method in interface com.tractrac.model.lib.api.spatial.IGeoCoordinate
    @@ -487,11 +491,14 @@
    getRaw() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    Gets the sensor data in a raw format.
    +
    + Gets the sensor data in a raw format.
    getRideHeight() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    Gets the ride height
    +
    + Gets the ride height, defined as the distance between the base of the mast + and the water.
    getRoute(UUID) - Method in interface com.tractrac.model.lib.api.event.IEvent
    @@ -639,7 +646,9 @@
    getTrim() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    Gets the trim
    +
    + Gets the trim, defined as the up/down rotation of a vessel about its + lateral/Y (side-to-side or port-starboard) axis.
    getType() - Method in interface com.tractrac.subscription.lib.api.event.IStoredDataEvent
    @@ -688,14 +697,6 @@ Coming up: if the race has the ONLINE value in the event manager and it is coming up
  • -
    getVMG() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    -
    Gets the VMG
    -
    -
    getWind() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    -
    -
    Gets the wind
    -
    getX() - Method in interface com.tractrac.model.lib.api.spatial.ICoordinate
    The value of the x coordinate
    diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html index 9cc5f8fdeab..49187d3a193 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html @@ -2,9 +2,9 @@ - + H-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html index 1839be39b50..5e6716423d5 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html @@ -2,9 +2,9 @@ - + I-Index - + @@ -358,7 +358,8 @@
    ISensorData - Interface in com.tractrac.model.lib.api.sensor
    -
    This interface implements sensor data.
    +
    + This interface implements sensor data.
    IServerTimeListener - Interface in com.tractrac.subscription.lib.api.event
    diff --git a/java/com.tractrac.clientmodule/javadoc/index.html b/java/com.tractrac.clientmodule/javadoc/index.html index 510f94ea79b..a7c58225b4c 100755 --- a/java/com.tractrac.clientmodule/javadoc/index.html +++ b/java/com.tractrac.clientmodule/javadoc/index.html @@ -2,7 +2,7 @@ - + Generated Documentation (Untitled) diff --git a/java/com.tractrac.clientmodule/javadoc/overview-summary.html b/java/com.tractrac.clientmodule/javadoc/overview-summary.html index 1aa2941dee8..468a5760995 100755 --- a/java/com.tractrac.clientmodule/javadoc/overview-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/overview-summary.html @@ -2,9 +2,9 @@ - + Overview - + diff --git a/java/com.tractrac.clientmodule/javadoc/overview-tree.html b/java/com.tractrac.clientmodule/javadoc/overview-tree.html index f06474e318a..77ff40d4f33 100755 --- a/java/com.tractrac.clientmodule/javadoc/overview-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/overview-tree.html @@ -2,9 +2,9 @@ - + Class Hierarchy - + diff --git a/java/com.tractrac.clientmodule/javadoc/serialized-form.html b/java/com.tractrac.clientmodule/javadoc/serialized-form.html index 99e7b4fd22f..9d5e4deb8e8 100755 --- a/java/com.tractrac.clientmodule/javadoc/serialized-form.html +++ b/java/com.tractrac.clientmodule/javadoc/serialized-form.html @@ -2,9 +2,9 @@ - + Serialized Form - + From 4c77c1afe1a03224bc2ba7126d86b342ecf7196c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 1 Mar 2018 12:13:28 +0100 Subject: [PATCH 202/214] fixed typo in variable name Change-Id: Iad760ee8018764857fc9f190c8f52d2dc2172f7e --- .../sailing/domain/common/tracking/impl/BravoFixImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/BravoFixImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/BravoFixImpl.java index 7db57751513..1d980ff8b2a 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/BravoFixImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/BravoFixImpl.java @@ -27,15 +27,15 @@ public class BravoFixImpl extends SensorFixImpl implements BravoFix { @Override public Distance getRideHeight() { - Double rideHeightPortHullasDouble = fix.get(BravoSensorDataMetadata.RIDE_HEIGHT_PORT_HULL.getColumnIndex()); + Double rideHeightPortHullAsDouble = fix.get(BravoSensorDataMetadata.RIDE_HEIGHT_PORT_HULL.getColumnIndex()); Double rideHeightStarboardHullAsDouble = fix .get(BravoSensorDataMetadata.RIDE_HEIGHT_STBD_HULL.getColumnIndex()); - return rideHeightPortHullasDouble == null ? + return rideHeightPortHullAsDouble == null ? rideHeightStarboardHullAsDouble == null ? null : new MeterDistance(rideHeightStarboardHullAsDouble) : rideHeightStarboardHullAsDouble == null ? - new MeterDistance(rideHeightPortHullasDouble) : - new MeterDistance(Math.min(rideHeightPortHullasDouble, rideHeightStarboardHullAsDouble)); + new MeterDistance(rideHeightPortHullAsDouble) : + new MeterDistance(Math.min(rideHeightPortHullAsDouble, rideHeightStarboardHullAsDouble)); } @Override From 06cb357fe5298ca3673f4e70248155e58b92671b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 1 Mar 2018 17:42:49 +0100 Subject: [PATCH 203/214] aftermath of Jetty upgrade: the start script required an update because the Jetty port needs to be patched into a different file now Change-Id: I4e563862e1924cff047a20362fab5dedd31b4340 --- java/target/start | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/target/start b/java/target/start index abc40dc289b..6d2ca902c50 100755 --- a/java/target/start +++ b/java/target/start @@ -62,7 +62,7 @@ sed $SED_ARGS "/replication.exchangeName/d" ./configuration/config.ini sed $SED_ARGS "/replication.exchangeHost/d" ./configuration/config.ini # Ensure that Jetty Port is set correctly -sed $SED_ARGS "s/^.*jetty.port.*$/<\/Set>/g" ./configuration/jetty/etc/jetty-selector.xml +sed $SED_ARGS "s/^.*jetty.http.port.*$/<\/Set>/g" ./configuration/jetty/etc/jetty-http.xml # Update monitoring with the right ports sed $SED_ARGS "s/127.0.0.1:[0-9][0-9][0-9][0-9]\//127.0.0.1:$SERVER_PORT\//g" ./configuration/monitoring.properties From 24f6f360ad18196d5ac2d02ad6e78a29aa62d295 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 1 Mar 2018 17:51:05 +0100 Subject: [PATCH 204/214] fixed link to competitor add service doc Change-Id: I745a32fa5959f283a83bf3c9bf2c4f77aa34c009 --- .../webservices/api/v1/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html index 9501aa42504..ef2e0b2716e 100644 --- a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html @@ -134,7 +134,7 @@ name to make sure you get to the event you're interested in. Obtains the competitors participating in a regatta - /api/v1/regattas/{regattaname}/competitors/createandadd + /api/v1/regattas/{regattaname}/competitors/createandadd Creates a competitor and adds it to the regatta From a4d38855fbcee295ff0c70c182749f6608b1b7cc Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 2 Mar 2018 10:09:49 +0100 Subject: [PATCH 205/214] fixed two more links in index.html for web service docs Change-Id: Ic923bdc2c2c477af106da2b161b60d71ef108372 --- .../webservices/api/v1/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html index ef2e0b2716e..074f64505f8 100644 --- a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html @@ -122,11 +122,11 @@ name to make sure you get to the event you're interested in. Executes the query for the regatta and gets the result - /api/v1/regattas/{regattaname}/addracecolumns + /api/v1/regattas/{regattaname}/addracecolumns Adds one or more race columns to a regatta - /api/v1/regattas/{regattaname}/removeracecolumn + /api/v1/regattas/{regattaname}/removeracecolumn Removes a race column from a regatta From 4a8852507a79d6940dfe3c012998bf4eb85e4e00 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 2 Mar 2018 10:32:55 +0100 Subject: [PATCH 206/214] fixed typo in release_notes_admin.html Change-Id: Icba090da0e83b9e7049c486413c8affb3ad1f6d5 --- java/com.sap.sailing.www/release_notes_admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index d450027fbe9..c34c904bc85 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -41,7 +41,7 @@

    January 2018

    com.tractrac.model.lib.api.sensor - package com.tractrac.model.lib.api.sensor
    -
     
    +
    +
    + Sensor data interfaces
    +
    com.tractrac.model.lib.api.spatial - package com.tractrac.model.lib.api.spatial
    @@ -142,7 +145,10 @@ Subscriptions related with the races
    com.tractrac.subscription.lib.api.route - package com.tractrac.subscription.lib.api.route
    -
     
    +
    +
    + Subscriptions related with the routes
    +
    createCompressedPosition(double, double, double, double, double) - Method in interface com.tractrac.model.lib.api.data.IPositionFactory
    Create a compressed position that is a position that can be serialized and diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html index a64377a8920..0f2aab2b2bf 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-4.html @@ -2,9 +2,9 @@ - + D-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html index c580af62957..f58324406df 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-5.html @@ -2,9 +2,9 @@ - + E-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html index a2ab5a62f09..8d0d5621170 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-6.html @@ -2,9 +2,9 @@ - + F-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html index 5ca7ad07f28..08fe7bfe401 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-7.html @@ -2,9 +2,9 @@ - + G-Index - + @@ -494,11 +494,17 @@
    Gets the sensor data in a raw format.
    -
    getRideHeight() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    +
    getRideHeightPort() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    - Gets the ride height, defined as the distance between the base of the mast - and the water.
    + Gets the perpendicular distance from the bottom of the port-hull + to the water surface. +
    +
    getRideHeightStarboard() - Method in interface com.tractrac.model.lib.api.sensor.ISensorData
    +
    +
    + Gets the perpendicular distance from the bottom of the starboard-hull + to the water surface.
    getRoute(UUID) - Method in interface com.tractrac.model.lib.api.event.IEvent
    diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html index 49187d3a193..8e505408b3e 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-8.html @@ -2,9 +2,9 @@ - + H-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html b/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html index 5e6716423d5..630338b3df8 100755 --- a/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html +++ b/java/com.tractrac.clientmodule/javadoc/index-files/index-9.html @@ -2,9 +2,9 @@ - + I-Index - + diff --git a/java/com.tractrac.clientmodule/javadoc/index.html b/java/com.tractrac.clientmodule/javadoc/index.html index a7c58225b4c..6e3820b553e 100755 --- a/java/com.tractrac.clientmodule/javadoc/index.html +++ b/java/com.tractrac.clientmodule/javadoc/index.html @@ -2,7 +2,7 @@ - + Generated Documentation (Untitled) diff --git a/java/com.tractrac.clientmodule/javadoc/overview-summary.html b/java/com.tractrac.clientmodule/javadoc/overview-summary.html index 468a5760995..3bc03c7a133 100755 --- a/java/com.tractrac.clientmodule/javadoc/overview-summary.html +++ b/java/com.tractrac.clientmodule/javadoc/overview-summary.html @@ -2,9 +2,9 @@ - + Overview - + @@ -120,7 +120,10 @@ com.tractrac.model.lib.api.sensor -  + +
    + Sensor data interfaces
    + com.tractrac.model.lib.api.spatial @@ -166,7 +169,10 @@ com.tractrac.subscription.lib.api.route -  + +
    + Subscriptions related with the routes
    + diff --git a/java/com.tractrac.clientmodule/javadoc/overview-tree.html b/java/com.tractrac.clientmodule/javadoc/overview-tree.html index 77ff40d4f33..15b46c0d56d 100755 --- a/java/com.tractrac.clientmodule/javadoc/overview-tree.html +++ b/java/com.tractrac.clientmodule/javadoc/overview-tree.html @@ -2,9 +2,9 @@ - + Class Hierarchy - + @@ -337,14 +337,14 @@ diff --git a/java/com.tractrac.clientmodule/javadoc/serialized-form.html b/java/com.tractrac.clientmodule/javadoc/serialized-form.html index 9d5e4deb8e8..41b76082098 100755 --- a/java/com.tractrac.clientmodule/javadoc/serialized-form.html +++ b/java/com.tractrac.clientmodule/javadoc/serialized-form.html @@ -2,9 +2,9 @@ - + Serialized Form - + diff --git a/java/com.tractrac.clientmodule/lib/TracAPI-src.jar b/java/com.tractrac.clientmodule/lib/TracAPI-src.jar index b1ec60457ed0ed8b2eece4a83a86d9ce1365e862..31f01cdeaf22065c1721aefab7c0275e9cf277d5 100755 GIT binary patch delta 5066 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 delta 4684 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 U+41s`jfE0_fkdYPpUKhx0L`C+b^rhX diff --git a/java/com.tractrac.clientmodule/lib/TracAPI.jar b/java/com.tractrac.clientmodule/lib/TracAPI.jar index a809e3e44575f05853a5b8f3495b36f7b6148e98..290f947d14e2cede89b592e79b47f88a62b926d6 100755 GIT binary patch delta 155673 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%0AZ|Ma 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
    DOXSJ(CwvCm^$@`>owYS}Ag1cWp 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

    ZvXsr{FUif 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

  • 2uT`vVy)ktK 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 VFySL{`S6x5>zQlaRr!3t{Qu;JK?eW; diff --git a/java/com.tractrac.clientmodule/pom.xml b/java/com.tractrac.clientmodule/pom.xml index a2fce5ff671..83574fa3088 100755 --- a/java/com.tractrac.clientmodule/pom.xml +++ b/java/com.tractrac.clientmodule/pom.xml @@ -8,6 +8,6 @@ 1.0.0-SNAPSHOT com.tractrac.clientmodule - 3.9.0 + 3.10.0 eclipse-plugin From 2a59d21e00c8fe3ccadbc5b41dc20439efc74fc3 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 3 Mar 2018 11:45:33 +0000 Subject: [PATCH 210/214] Updated home (markdown) --- home.md | 1 + 1 file changed, 1 insertion(+) diff --git a/home.md b/home.md index 6dc5626ac91..72eef793135 100644 --- a/home.md +++ b/home.md @@ -35,6 +35,7 @@ SAP is at the center of today’s technology revolution, developing innovations * [[Creating an EC2 image from scratch|wiki/info/landscape/creating-ec2-image-from-scratch]] * [[Creating a webserver EC2 image from scratch|wiki/info/landscape/creating-ec2-image-for-webserver-from-scratch]] * [[EC2 mail relaying|wiki/info/landscape/mail-relaying]] + * [[Setting up dedicated S3 buckets|wiki/info/landscape/s3-bucket-setup]] * [[Building and Deploying|wiki/info/landscape/building-and-deploying]] * [[Data Mining Architecture|wiki/info/landscape/data-mining-architecture]] * [[Typical Data Mining Scenarios|wiki/info/landscape/typical-data-mining-scenarios]] From 2e3f5c7cbb6ce232b4c147eb61dd47cea0395750 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 3 Mar 2018 14:41:43 +0100 Subject: [PATCH 211/214] a first draft of an implementation capable of receiving TracTrac sensor data Change-Id: I6069dea79ac770949a0108fe59ca00a1ca24b109 --- .../BravoExtendedSensorDataMetadata.java | 3 + .../domain/tractracadapter/ReceiverType.java | 2 +- .../impl/DomainFactoryImpl.java | 6 +- .../impl/RawPositionReceiver.java | 44 +++---- .../impl/SensorDataReceiver.java | 117 ++++++++++++++++++ .../META-INF/MANIFEST.MF | 1 + 6 files changed, 149 insertions(+), 24 deletions(-) create mode 100755 java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoExtendedSensorDataMetadata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoExtendedSensorDataMetadata.java index efd9c9a863d..9cc436c2cdd 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoExtendedSensorDataMetadata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoExtendedSensorDataMetadata.java @@ -20,10 +20,13 @@ import com.sap.sailing.domain.common.tracking.DoubleVectorFix; * into the {@link DoubleVectorFix}. */ public enum BravoExtendedSensorDataMetadata implements ColumnMetadata { + // START ----- DONT CHANGE UNLESS BravoSensorDataMetadata CHANGES TOO ------ RIDE_HEIGHT_PORT_HULL("RideHeightPort", /* expectedInLogfile */ true), // RIDE_HEIGHT_STBD_HULL("RideHeightStbd", /* expectedInLogfile */ true), // HEEL("Heel", /* expectedInLogfile */ true), // PITCH("PitchRate", /* expectedInLogfile */ true), // + // END ----- DONT CHANGE UNLESS BravoSensorDataMetadata CHANGES TOO ------ + DB_RAKE_PORT("DaggerBoardRakeAnglePort", /* expectedInLogfile */ true), // DB_RAKE_STBD("DaggerBoardRakeAngleStbd", /* expectedInLogfile */ true), // RUDDER_RAKE_PORT("RudderRakeAnglePort", /* expectedInLogfile */ true), // diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/ReceiverType.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/ReceiverType.java index 3062fbcb2ca..983bb116c3b 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/ReceiverType.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/ReceiverType.java @@ -1,5 +1,5 @@ package com.sap.sailing.domain.tractracadapter; public enum ReceiverType { - MARKPASSINGS, MARKPOSITIONS, RACECOURSE, RACESTARTFINISH, RAWPOSITIONS + MARKPASSINGS, MARKPOSITIONS, RACECOURSE, RACESTARTFINISH, RAWPOSITIONS, SENSORDATA } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index 093d59e7c46..85a2c91365a 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -430,7 +430,11 @@ public class DomainFactoryImpl implements DomainFactory { result.add(new RaceStartedAndFinishedReceiver( trackedRegatta, tractracEvent, simulator, this, eventSubscriber, raceSubscriber, timeoutInMilliseconds)); break; - } + case SENSORDATA: + result.add(new SensorDataReceiver( + trackedRegatta, tractracEvent, simulator, this, eventSubscriber, raceSubscriber, timeoutInMilliseconds)); + break; + } } return result; } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java index 651c3830da8..ab24e3bed53 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java @@ -48,27 +48,27 @@ public class RawPositionReceiver extends AbstractReceiverWithQueue event) { - if (!event.getA().getCompetitor().isNonCompeting()) { - if (received++ % 1000 == 0) { - System.out.print("P"); - if ((received / 1000 + 1) % 80 == 0) { - System.out.println(); - } - } - IRace race = event.getA().getRace(); - DynamicTrackedRace trackedRace = getTrackedRace(race); - if (trackedRace != null) { - final GPSFixMoving fix = getDomainFactory().createGPSFixMoving(event.getB()); - Competitor competitor = getDomainFactory().getOrCreateCompetitor(event.getA().getCompetitor()); - if (getSimulator() != null) { - getSimulator().scheduleCompetitorPosition(competitor, fix); - } else { - trackedRace.recordFix(competitor, fix); - } - } else { - logger.warning("Couldn't find tracked race for race " + race.getName() - + ". Dropping raw position event " + event); - } - } + if (!event.getA().getCompetitor().isNonCompeting()) { + if (received++ % 1000 == 0) { + System.out.print("P"); + if ((received / 1000 + 1) % 80 == 0) { + System.out.println(); + } + } + IRace race = event.getA().getRace(); + DynamicTrackedRace trackedRace = getTrackedRace(race); + if (trackedRace != null) { + final GPSFixMoving fix = getDomainFactory().createGPSFixMoving(event.getB()); + Competitor competitor = getDomainFactory().getOrCreateCompetitor(event.getA().getCompetitor()); + if (getSimulator() != null) { + getSimulator().scheduleCompetitorPosition(competitor, fix); + } else { + trackedRace.recordFix(competitor, fix); + } + } else { + logger.warning("Couldn't find tracked race for race " + race.getName() + + ". Dropping raw position event " + event); + } + } } } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java new file mode 100755 index 00000000000..ca67729bb67 --- /dev/null +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java @@ -0,0 +1,117 @@ +package com.sap.sailing.domain.tractracadapter.impl; + +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.sensordata.BravoSensorDataMetadata; +import com.sap.sailing.domain.common.tracking.BravoFix; +import com.sap.sailing.domain.common.tracking.impl.BravoFixImpl; +import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl; +import com.sap.sailing.domain.tracking.BravoFixTrack; +import com.sap.sailing.domain.tracking.DynamicBravoFixTrack; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.impl.CompetitorBravoFixTrackImpl; +import com.sap.sailing.domain.tractracadapter.DomainFactory; +import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.tractrac.model.lib.api.event.IEvent; +import com.tractrac.model.lib.api.event.IRace; +import com.tractrac.model.lib.api.event.IRaceCompetitor; +import com.tractrac.model.lib.api.sensor.ISensorData; +import com.tractrac.subscription.lib.api.IEventSubscriber; +import com.tractrac.subscription.lib.api.IRaceSubscriber; +import com.tractrac.subscription.lib.api.competitor.ICompetitorSensorDataListener; + +/** + * Subscribes an + * + * @author Axel Uhl (d043530) + * + */ +public class SensorDataReceiver extends AbstractReceiverWithQueue { + private static final Logger logger = Logger.getLogger(SensorDataReceiver.class.getName()); + private final ICompetitorSensorDataListener listener; + private int received; + + public SensorDataReceiver(DynamicTrackedRegatta trackedRegatta, IEvent tractracEvent, + Simulator simulator, DomainFactory domainFactory, IEventSubscriber eventSubscriber, + IRaceSubscriber raceSubscriber, long timeoutInMilliseconds) { + super(domainFactory, tractracEvent, trackedRegatta, simulator, eventSubscriber, raceSubscriber, timeoutInMilliseconds); + listener = new ICompetitorSensorDataListener() { + @Override + public void gotSensorData(IRaceCompetitor raceCompetitor, ISensorData sensorData) { + enqueue(new Triple<>(raceCompetitor, sensorData, null)); + } + }; + } + + @Override + public void subscribe() { + getRaceSubscriber().subscribeCompetitorSensorData(listener); + startThread(); + } + + @Override + protected void unsubscribe() { + getRaceSubscriber().unsubscribeCompetitorSensorData(listener); + } + + /** + * Looks for valid, non-{@code null} components for ride height port/starboard, heel and trim + * in the {@link ISensorData} object. All those values are copied into a {@link BravoFix} and + * inserted into the competitor's bravo fix track. + */ + @Override + protected void handleEvent(Triple event) { + if (received++ % 1000 == 0) { + System.out.print("S"); + if ((received / 1000 + 1) % 80 == 0) { + System.out.println(); + } + } + final IRace race = event.getA().getRace(); + DynamicTrackedRace trackedRace = getTrackedRace(race); + if (trackedRace != null) { + final ISensorData sensorData = event.getB(); + if (sensorData != null) { + final Float rideHeightPortInMeters = sensorData.getRideHeightPort(); + final Float rideHeightStarboardInMeters = sensorData.getRideHeightStarboard(); + final Float heelInDegrees = sensorData.getHeel(); + final Float trimInDegrees = sensorData.getTrim(); + final Double[] fixData = new Double[BravoSensorDataMetadata.getTrackColumnCount()]; + boolean create = false; // only create fix if at least one metric is present + // fill the port/starboard columns as well because their minimum defines the true ride height + if (rideHeightPortInMeters != null) { + fixData[BravoSensorDataMetadata.RIDE_HEIGHT_PORT_HULL.getColumnIndex()] = Double.valueOf(rideHeightPortInMeters); + create = true; + } + if (rideHeightStarboardInMeters != null) { + fixData[BravoSensorDataMetadata.RIDE_HEIGHT_STBD_HULL.getColumnIndex()] = Double.valueOf(rideHeightStarboardInMeters); + create = true; + } + if (heelInDegrees != null) { + fixData[BravoSensorDataMetadata.HEEL.getColumnIndex()] = Double.valueOf(heelInDegrees); + create = true; + } + if (trimInDegrees != null) { + fixData[BravoSensorDataMetadata.PITCH.getColumnIndex()] = Double.valueOf(trimInDegrees); + create = true; + } + if (create) { + final BravoFix fix = new BravoFixImpl(new DoubleVectorFixImpl(new MillisecondsTimePoint(sensorData.getTimestamp()), fixData)); + final Competitor competitor = getDomainFactory().getOrCreateCompetitor(event.getA().getCompetitor()); + DynamicBravoFixTrack bravoFixTrack = + trackedRace.getOrCreateSensorTrack(competitor, BravoFixTrack.TRACK_NAME, + () -> new CompetitorBravoFixTrackImpl(competitor, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ false, + trackedRace.getTrack(competitor))); + bravoFixTrack.add(fix); + } + } + } else { + logger.warning("Couldn't find tracked race for race " + race.getName() + + ". Dropping sensor event " + event); + } + } + +} diff --git a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF index b1b13765e20..318b514342b 100644 --- a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF +++ b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF @@ -47,6 +47,7 @@ Export-Package: com.tractrac.asio.lib.api, com.tractrac.model.lib.api.event, com.tractrac.model.lib.api.metadata, com.tractrac.model.lib.api.route, + com.tractrac.model.lib.api.sensor, com.tractrac.model.lib.api.spatial, com.tractrac.model.lib.impl, com.tractrac.model.lib.impl.attachment, From 7f84c048c2ec7faa28d9318a0a96def48d34d240 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 3 Mar 2018 14:43:35 +0100 Subject: [PATCH 212/214] included SENSORDATA in the list of receivers loaded by default Change-Id: I2154a4a54c6074f2fff1e49b3aa2dc189f66ed68 --- .../sailing/domain/tractracadapter/impl/DomainFactoryImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index 85a2c91365a..c65b8bd83cd 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -450,7 +450,8 @@ public class DomainFactoryImpl implements DomainFactory { raceDefinitionSetToUpdate, trackedRegattaRegistry, raceLogResolver, courseDesignUpdateURI, tracTracUsername, tracTracPassword, eventSubscriber, raceSubscriber, useInternalMarkPassingAlgorithm, timeoutInMilliseconds, ReceiverType.RACECOURSE, - ReceiverType.MARKPASSINGS, ReceiverType.MARKPOSITIONS, ReceiverType.RACESTARTFINISH, ReceiverType.RAWPOSITIONS); + ReceiverType.MARKPASSINGS, ReceiverType.MARKPOSITIONS, ReceiverType.RACESTARTFINISH, ReceiverType.RAWPOSITIONS, + ReceiverType.SENSORDATA); } @Override From f878cd5d478d47a1d738f64745c691c2cd0f96ea Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 3 Mar 2018 14:54:46 +0100 Subject: [PATCH 213/214] added simulator support for TracTrac sensor data Change-Id: I12720e4fe4f6834e2f400eb77075523f3f068ad9 --- .../domain/common/tracking/SensorFix.java | 6 +++ .../common/tracking/impl/SensorFixImpl.java | 7 ++++ .../impl/SensorDataReceiver.java | 6 ++- .../tractracadapter/impl/Simulator.java | 37 ++++++++++++++++--- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/SensorFix.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/SensorFix.java index 0eba757cef5..ea79e71c685 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/SensorFix.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/SensorFix.java @@ -21,4 +21,10 @@ public interface SensorFix extends Timed { */ double get(String valueName) throws IllegalArgumentException; + /** + * Obtains the data array containing the raw values making this sensor fix. + * The array returned is a copy, and writing to it will not affect this fix + * in any way. + */ + Double[] get(); } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/SensorFixImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/SensorFixImpl.java index b25d68eda28..581ebcf5759 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/SensorFixImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/SensorFixImpl.java @@ -26,6 +26,13 @@ public abstract class SensorFixImpl implements SensorFix { return fix.getTimePoint(); } + @Override + public Double[] get() { + Double[] result = new Double[fix.get().length]; + System.arraycopy(fix.get(), 0, result, 0, result.length); + return result; + } + @Override public double get(String valueName) { final ColumnMetadata columnMetadata = resolveMetadataFromValueName(valueName); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java index ca67729bb67..495a4f3d708 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/SensorDataReceiver.java @@ -105,7 +105,11 @@ public class SensorDataReceiver extends AbstractReceiverWithQueue new CompetitorBravoFixTrackImpl(competitor, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ false, trackedRace.getTrack(competitor))); - bravoFixTrack.add(fix); + if (getSimulator() != null) { + getSimulator().scheduleCompetitorSensorData(bravoFixTrack, fix); + } else { + bravoFixTrack.add(fix); + } } } } else { diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/Simulator.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/Simulator.java index d26d28b1909..b8110005f68 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/Simulator.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/Simulator.java @@ -14,8 +14,12 @@ import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sailing.domain.common.tracking.BravoFix; import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.common.tracking.impl.BravoFixImpl; +import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.tracking.DynamicBravoFixTrack; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.RaceListener; @@ -272,20 +276,40 @@ public class Simulator { scheduleFixRecording(mark, markFix, recorder); } - private void scheduleFixRecording(final T object, GPSFixMoving fix, final RecordGPSFix recorder) { + public void scheduleCompetitorSensorData(DynamicBravoFixTrack bravoFixTrack, BravoFix fix) { final TimePoint transformedTimepoint = advance(fix.getTimePoint()); - final GPSFixMoving transformedMarkFix = new GPSFixMovingImpl(fix.getPosition(), transformedTimepoint, fix.getSpeed()); - long waitTime = getWaitTimeInMillisUntil(transformedMarkFix.getTimePoint()); + final BravoFix transformedFix = new BravoFixImpl(new DoubleVectorFixImpl(transformedTimepoint, fix.get())); + long waitTime = getWaitTimeInMillisUntil(transformedFix.getTimePoint()); if (waitTime <= 0) { - recorder.recordFix(object, transformedMarkFix); + bravoFixTrack.add(transformedFix); } else { timer.schedule(new TimerTask() { @Override public void run() { try { - recorder.recordFix(object, transformedMarkFix); + bravoFixTrack.add(transformedFix); } catch (Exception e) { - logger.throwing(Simulator.class.getName(), "scheduleMarkPosition", e); + logger.throwing(Simulator.class.getName(), "scheduleSensorData", e); + } + } + }, transformedTimepoint.asDate()); + } + } + + private void scheduleFixRecording(final T object, GPSFixMoving fix, final RecordGPSFix recorder) { + final TimePoint transformedTimepoint = advance(fix.getTimePoint()); + final GPSFixMoving transformedFix = new GPSFixMovingImpl(fix.getPosition(), transformedTimepoint, fix.getSpeed()); + long waitTime = getWaitTimeInMillisUntil(transformedFix.getTimePoint()); + if (waitTime <= 0) { + recorder.recordFix(object, transformedFix); + } else { + timer.schedule(new TimerTask() { + @Override + public void run() { + try { + recorder.recordFix(object, transformedFix); + } catch (Exception e) { + logger.throwing(Simulator.class.getName(), "schedulePosition", e); } } }, transformedTimepoint.asDate()); @@ -297,4 +321,5 @@ public class Simulator { timer.cancel(); stopped = true; } + } From fe86efe92db7b377d96ec92c4ee9ec8d2e2d648b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 3 Mar 2018 21:26:13 +0100 Subject: [PATCH 214/214] added missing implementation of new get() method of SensorFix interface Change-Id: Ie0dde9d6571892e2c70cf1603c11837870c553ca --- .../racelogtracking/test/impl/SensorFixStoreAndLoadTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java index 383919426b2..1ec8c9a5c6e 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java @@ -505,6 +505,11 @@ public class SensorFixStoreAndLoadTest { return fix.get(TEST_COLUMN_INDEX); } + @Override + public Double[] get() { + return Arrays.copyOf(fix.get(), fix.get().length); + } + } public class TestFixTrackImpl extends SensorFixTrackImpl {