diff --git a/pom.xml b/pom.xml index aacfdfd..8a4aacf 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,16 @@ 24.6.4 + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + org.springframework.boot @@ -31,6 +41,11 @@ com.vaadin vaadin-spring-boot-starter + + com.github.appreciated + apexcharts + 24.0.1 + org.mariadb.jdbc diff --git a/src/main/frontend/prefers-color-scheme.js b/src/main/frontend/prefers-color-scheme.js new file mode 100644 index 0000000..61db7da --- /dev/null +++ b/src/main/frontend/prefers-color-scheme.js @@ -0,0 +1,12 @@ +window.applyTheme = () => { + const theme = window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : ""; + document.documentElement.setAttribute("theme", theme); +}; +window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener('change', function () { + window.applyTheme() + }); +window.applyTheme(); \ No newline at end of file diff --git a/src/main/java/org/kickerelo/kickerelo/KickerEloService.java b/src/main/java/org/kickerelo/kickerelo/KickerEloService.java deleted file mode 100644 index 0edfe88..0000000 --- a/src/main/java/org/kickerelo/kickerelo/KickerEloService.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.kickerelo.kickerelo; - -import org.kickerelo.kickerelo.data.Ergebnis1vs1; -import org.kickerelo.kickerelo.data.Ergebnis2vs2; -import org.kickerelo.kickerelo.data.Spieler; -import org.kickerelo.kickerelo.repository.Ergebnis1vs1Repository; -import org.kickerelo.kickerelo.repository.Ergebnis2vs2Repository; -import org.kickerelo.kickerelo.repository.SpielerRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class KickerEloService { - @Autowired - private Ergebnis1vs1Repository ergebnis1vs1Repository; - @Autowired - private Ergebnis2vs2Repository ergebnis2vs2Repository; - @Autowired - private SpielerRepository spielerRepository; - - public List getSpielerNamen() { - return spielerRepository.findAll().stream().map(Spieler::getName).toList(); - } - - public void enterResult1vs1(String gewinnerName, String verliererName, - short toreVerlierer) { - - Spieler gewinner = spielerRepository.findByName(gewinnerName) - .orElseThrow(() -> new NoSuchPlayerException(gewinnerName)); - - Spieler verlierer = spielerRepository.findByName(verliererName) - .orElseThrow(() -> new NoSuchPlayerException(verliererName)); - - - Ergebnis1vs1 ergebnis = new Ergebnis1vs1(gewinner, verlierer, toreVerlierer); - - ergebnis1vs1Repository.save(ergebnis); - - // Compute the new ELO and update the Spieler entities - } - - public void enterResult2vs2(String gewinnerNameVorn, String gewinnerNameHinten, - String verliererNameVorn, String verliererNameHinten, - short toreVerlierer) { - - Spieler gewinnerVorn = spielerRepository.findByName(gewinnerNameVorn) - .orElseThrow(() -> new NoSuchPlayerException(gewinnerNameVorn)); - - Spieler gewinnerHinten = spielerRepository.findByName(gewinnerNameHinten) - .orElseThrow(() -> new NoSuchPlayerException(gewinnerNameHinten)); - - Spieler verliererVorn = spielerRepository.findByName(verliererNameVorn) - .orElseThrow(() -> new NoSuchPlayerException(verliererNameVorn)); - - Spieler verliererHinten = spielerRepository.findByName(verliererNameHinten) - .orElseThrow(() -> new NoSuchPlayerException(verliererNameHinten)); - - Ergebnis2vs2 ergebnis = new Ergebnis2vs2(gewinnerVorn, gewinnerHinten, verliererVorn, verliererHinten, toreVerlierer); - - ergebnis2vs2Repository.save(ergebnis); - - // Compute the new ELO, update the Spieler entitities - } - - public void addSpieler(String name) { - Spieler spieler = new Spieler(); - spieler.setName(name); - spieler.setElo(1500); - spielerRepository.save(spieler); - } -} diff --git a/src/main/java/org/kickerelo/kickerelo/data/Spieler.java b/src/main/java/org/kickerelo/kickerelo/data/Spieler.java index a0f64b1..e6ad958 100644 --- a/src/main/java/org/kickerelo/kickerelo/data/Spieler.java +++ b/src/main/java/org/kickerelo/kickerelo/data/Spieler.java @@ -13,8 +13,11 @@ public class Spieler { @Column(name = "NAME", nullable = false, unique = true) private String name; - @Column(name = "ELO", nullable = false) - private float elo; + @Column(name = "ELO1vs1", nullable = false) + private float elo1vs1; + + @Column(name = "ELO2vs2", nullable = false) + private float elo2vs2; @Column(name = "ELO_ALT") private float elo_alt; @@ -38,12 +41,20 @@ public class Spieler { this.name = name; } - public float getElo() { - return elo; + public float getElo1vs1() { + return elo1vs1; } - public void setElo(float elo) { - this.elo = elo; + public void setElo1vs1(float elo) { + this.elo1vs1 = elo; + } + + public float getElo2vs2() { + return elo2vs2; + } + + public void setElo2vs2(float elo2vs2) { + this.elo2vs2 = elo2vs2; } public float getElo_alt() { diff --git a/src/main/java/org/kickerelo/kickerelo/data/update-schema.sql b/src/main/java/org/kickerelo/kickerelo/data/update-schema.sql index 771b9d4..c056b5d 100644 --- a/src/main/java/org/kickerelo/kickerelo/data/update-schema.sql +++ b/src/main/java/org/kickerelo/kickerelo/data/update-schema.sql @@ -30,7 +30,8 @@ CREATE TABLE spieler ( id INT NOT NULL, name VARCHAR(255) NOT NULL, - elo FLOAT NOT NULL, + elo1vs1 FLOAT NOT NULL, + elo2vs2 FLOAT NOT NULL, elo_alt FLOAT NULL, CONSTRAINT pk_spieler PRIMARY KEY (id) ); diff --git a/src/main/java/org/kickerelo/kickerelo/exception/DuplicatePlayerException.java b/src/main/java/org/kickerelo/kickerelo/exception/DuplicatePlayerException.java new file mode 100644 index 0000000..5a15e17 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/exception/DuplicatePlayerException.java @@ -0,0 +1,7 @@ +package org.kickerelo.kickerelo.exception; + +public class DuplicatePlayerException extends RuntimeException { + public DuplicatePlayerException(String message) { + super(message); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/exception/InvalidDataException.java b/src/main/java/org/kickerelo/kickerelo/exception/InvalidDataException.java new file mode 100644 index 0000000..b0163ef --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/exception/InvalidDataException.java @@ -0,0 +1,7 @@ +package org.kickerelo.kickerelo.exception; + +public class InvalidDataException extends RuntimeException { + public InvalidDataException(String message) { + super(message); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/NoSuchPlayerException.java b/src/main/java/org/kickerelo/kickerelo/exception/NoSuchPlayerException.java similarity index 77% rename from src/main/java/org/kickerelo/kickerelo/NoSuchPlayerException.java rename to src/main/java/org/kickerelo/kickerelo/exception/NoSuchPlayerException.java index 2fbbe3f..b2d08cd 100644 --- a/src/main/java/org/kickerelo/kickerelo/NoSuchPlayerException.java +++ b/src/main/java/org/kickerelo/kickerelo/exception/NoSuchPlayerException.java @@ -1,4 +1,4 @@ -package org.kickerelo.kickerelo; +package org.kickerelo.kickerelo.exception; public class NoSuchPlayerException extends RuntimeException { public NoSuchPlayerException(String message) { diff --git a/src/main/java/org/kickerelo/kickerelo/exception/PlayerNameNotSetException.java b/src/main/java/org/kickerelo/kickerelo/exception/PlayerNameNotSetException.java new file mode 100644 index 0000000..eda8b0b --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/exception/PlayerNameNotSetException.java @@ -0,0 +1,7 @@ +package org.kickerelo.kickerelo.exception; + +public class PlayerNameNotSetException extends RuntimeException { + public PlayerNameNotSetException(String message) { + super(message); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java b/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java new file mode 100644 index 0000000..8751077 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java @@ -0,0 +1,36 @@ +package org.kickerelo.kickerelo.layout; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.tabs.Tab; +import com.vaadin.flow.component.tabs.Tabs; +import com.vaadin.flow.router.Layout; +import com.vaadin.flow.router.RouterLink; +import org.kickerelo.kickerelo.views.*; + +@Layout +@JsModule("prefers-color-scheme.js") +public class KickerAppLayout extends AppLayout { + + public KickerAppLayout() { + DrawerToggle drawerToggle = new DrawerToggle(); + + H1 title = new H1("Kicker-ELO"); + title.getStyle().set("font-size", "var(--lumo-font-size-l)").set("margin", "0"); + + addToNavbar(drawerToggle, title); + + RouterLink enter1vs1 = new RouterLink("1 vs 1", Enter1vs1View.class); + RouterLink enter2vs2 = new RouterLink("2 vs 2", Enter2vs2View.class); + RouterLink playerList = new RouterLink("Spielerliste", PlayerListView.class); + RouterLink graph1vs1 = new RouterLink("Graph 1 vs 1", Graph1vs1View.class); + RouterLink graph2vs2 = new RouterLink("Graph 2 vs 2", Graph2vs2View.class); + RouterLink admin = new RouterLink("Verwaltung", AdminView.class); + + Tabs tabs = new Tabs(new Tab(playerList), new Tab(enter1vs1), new Tab(enter2vs2), new Tab(graph1vs1), new Tab(graph2vs2), new Tab(admin)); + tabs.setOrientation(Tabs.Orientation.VERTICAL); + addToDrawer(tabs); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/service/EloCalculationService.java b/src/main/java/org/kickerelo/kickerelo/service/EloCalculationService.java new file mode 100644 index 0000000..b586f3a --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/service/EloCalculationService.java @@ -0,0 +1,56 @@ +package org.kickerelo.kickerelo.service; + +import org.kickerelo.kickerelo.data.Spieler; +import org.springframework.stereotype.Service; + + +/** + * This contains the math for calculating ELO only. + */ +@Service +public class EloCalculationService { + private final float initialElo1vs1 = 1500; + private final float initialElo2vs2 = 1500; + + /** + * Updates the 1 vs 1 ELOs of the players according to the result of the game. + * @param gewinner The entity representing the winning player + * @param verlierer The entity representing the losing player + * @param toreVerlierer The number of goals of the losing player + */ + public void updateElo1vs1(Spieler gewinner, Spieler verlierer, short toreVerlierer) { + final float initialElo = 1500; + final float baseK = 50; + final float reductionPerGoal = 0.1f * baseK; + + final float finalK = baseK - (reductionPerGoal * toreVerlierer); + float expectedScoreWinner = (float) (1 / (1 + Math.pow(10, (verlierer.getElo1vs1() - gewinner.getElo1vs1()) / 400))); + float expectedScoreLoser = (float) (1 / (1 + Math.pow(10, (gewinner.getElo1vs1() - verlierer.getElo1vs1()) / 400))); + + gewinner.setElo1vs1(gewinner.getElo1vs1() + finalK * (1-expectedScoreWinner)); + verlierer.setElo1vs1(verlierer.getElo1vs1() - finalK * expectedScoreLoser); + } + + /** + * Updates the 2 vs 2 ELOs of the players according to the result of the game + * @param gewinnerVorn The winning offensive player + * @param gewinnerHinten The winning defensive player + * @param verliererVorn The losing offensive player + * @param verliererHinten The losing defensive player + * @param toreVerlierer The number of goals of the losing teams + */ + public void updateElo2vs2(Spieler gewinnerVorn, Spieler gewinnerHinten, Spieler verliererVorn, Spieler verliererHinten, short toreVerlierer) { + gewinnerVorn.setElo2vs2(gewinnerVorn.getElo2vs2() + 10 - toreVerlierer); + gewinnerHinten.setElo2vs2(gewinnerHinten.getElo2vs2() + 10 - toreVerlierer); + verliererVorn.setElo2vs2(verliererVorn.getElo2vs2() - 10 + toreVerlierer); + verliererHinten.setElo2vs2(verliererHinten.getElo2vs2()); + } + + public float getInitialElo1vs1() { + return initialElo1vs1; + } + + public float getInitialElo2vs2() { + return initialElo2vs2; + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/service/KickerEloService.java b/src/main/java/org/kickerelo/kickerelo/service/KickerEloService.java new file mode 100644 index 0000000..b6308e7 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/service/KickerEloService.java @@ -0,0 +1,198 @@ +package org.kickerelo.kickerelo.service; + +import org.kickerelo.kickerelo.exception.DuplicatePlayerException; +import org.kickerelo.kickerelo.exception.InvalidDataException; +import org.kickerelo.kickerelo.exception.NoSuchPlayerException; +import org.kickerelo.kickerelo.data.Ergebnis1vs1; +import org.kickerelo.kickerelo.data.Ergebnis2vs2; +import org.kickerelo.kickerelo.data.Spieler; +import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; +import org.kickerelo.kickerelo.repository.Ergebnis1vs1Repository; +import org.kickerelo.kickerelo.repository.Ergebnis2vs2Repository; +import org.kickerelo.kickerelo.repository.SpielerRepository; +import org.kickerelo.kickerelo.util.Ergebnis1vs1TimeComparator; +import org.kickerelo.kickerelo.util.Ergebnis2vs2TimeComparator; +import org.kickerelo.kickerelo.util.Spieler1vs1EloComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +/** + * Provides all functions for the application + */ +@Service +public class KickerEloService { + @Autowired + private Ergebnis1vs1Repository ergebnis1vs1Repository; + @Autowired + private Ergebnis2vs2Repository ergebnis2vs2Repository; + @Autowired + private SpielerRepository spielerRepository; + @Autowired + private EloCalculationService eloCalculationService; + + /** + * @return List of all player names sorted alphabetically + */ + public List getSpielerNamen() { + return spielerRepository.findAll().stream().map(Spieler::getName).sorted().toList(); + } + + /** + * @return List of all player entities sorted by 1 vs 1 ELO + */ + public List getSpielerEntities() { + return spielerRepository.findAll().stream().sorted(new Spieler1vs1EloComparator()).toList(); + } + + /** + * Enter a result of a 1 vs 1 game + * @param gewinnerName The name of the winning player + * @param verliererName The name of the losing player + * @param toreVerlierer The number of goals of the loser + */ + public void enterResult1vs1(String gewinnerName, String verliererName, + short toreVerlierer) { + // Check if the inputs are valid + if (gewinnerName == null || verliererName == null) { + throw new PlayerNameNotSetException("Alle Namen müssen gesetzt sein"); + } + + if (gewinnerName.equals(verliererName)) { + throw new DuplicatePlayerException("winner and loser identical"); + } + if (toreVerlierer > 9 || toreVerlierer < 0) { + throw new InvalidDataException("too many goals"); + } + + Spieler gewinner = spielerRepository.findByName(gewinnerName) + .orElseThrow(() -> new NoSuchPlayerException(gewinnerName)); + + Spieler verlierer = spielerRepository.findByName(verliererName) + .orElseThrow(() -> new NoSuchPlayerException(verliererName)); + + + Ergebnis1vs1 ergebnis = new Ergebnis1vs1(gewinner, verlierer, toreVerlierer); + ergebnis1vs1Repository.save(ergebnis); + + eloCalculationService.updateElo1vs1(gewinner, verlierer, toreVerlierer); + spielerRepository.save(gewinner); + spielerRepository.save(verlierer); + + } + + /** + * Enter the result of a 2 vs 2 game + * @param gewinnerNameVorn Name of the winning offensive player + * @param gewinnerNameHinten Name of the winning defensive player + * @param verliererNameVorn Name of the losing offensive player + * @param verliererNameHinten Name of the losing defensive player + * @param toreVerlierer Number of goals of the losing team + */ + public void enterResult2vs2(String gewinnerNameVorn, String gewinnerNameHinten, + String verliererNameVorn, String verliererNameHinten, + short toreVerlierer) { + // Check if the inputs are valid + if (gewinnerNameVorn == null || gewinnerNameHinten == null + || verliererNameVorn == null || verliererNameHinten == null) { + throw new PlayerNameNotSetException("Alle Namen müssen gesetzt sein"); + } + + if (gewinnerNameVorn.equals(gewinnerNameHinten) || + gewinnerNameVorn.equals(verliererNameVorn) || + gewinnerNameVorn.equals(verliererNameHinten) || + gewinnerNameHinten.equals(verliererNameVorn) || + gewinnerNameHinten.equals(verliererNameHinten) || + verliererNameVorn.equals(verliererNameHinten)) { + throw new DuplicatePlayerException("players must not be identical"); + } + + if (toreVerlierer > 9 || toreVerlierer < 0) { + throw new InvalidDataException("too many loser goals"); + } + + Spieler gewinnerVorn = spielerRepository.findByName(gewinnerNameVorn) + .orElseThrow(() -> new NoSuchPlayerException(gewinnerNameVorn)); + + Spieler gewinnerHinten = spielerRepository.findByName(gewinnerNameHinten) + .orElseThrow(() -> new NoSuchPlayerException(gewinnerNameHinten)); + + Spieler verliererVorn = spielerRepository.findByName(verliererNameVorn) + .orElseThrow(() -> new NoSuchPlayerException(verliererNameVorn)); + + Spieler verliererHinten = spielerRepository.findByName(verliererNameHinten) + .orElseThrow(() -> new NoSuchPlayerException(verliererNameHinten)); + + Ergebnis2vs2 ergebnis = new Ergebnis2vs2(gewinnerVorn, gewinnerHinten, verliererVorn, verliererHinten, toreVerlierer); + ergebnis2vs2Repository.save(ergebnis); + + eloCalculationService.updateElo2vs2(gewinnerVorn, gewinnerHinten, verliererVorn, verliererHinten, toreVerlierer); + spielerRepository.save(gewinnerVorn); + spielerRepository.save(gewinnerHinten); + spielerRepository.save(verliererVorn); + spielerRepository.save(verliererHinten); + } + + /** + * Add a new player to the system + * @param name Name of the new player + */ + public void addSpieler(String name) { + // Check if the player name is valid + if (name == null || name.isBlank()) { + throw new PlayerNameNotSetException("Leerer Name"); + } + if (getSpielerNamen().contains(name)) { + throw new DuplicatePlayerException("players must not be identical"); + } + if (name.length() > 30) { + throw new InvalidDataException("Zu lang"); + } + Spieler spieler = new Spieler(); + spieler.setName(name); + spieler.setElo1vs1(eloCalculationService.getInitialElo1vs1()); + spieler.setElo2vs2(eloCalculationService.getInitialElo2vs2()); + spielerRepository.save(spieler); + } + + /** + * Recalculate and overwrite all 1 vs 1 ELOs. + */ + public void recalculateAll1vs1() { + HashMap players = new HashMap<>(); + for (Spieler spieler : spielerRepository.findAll()) { + spieler.setElo1vs1(eloCalculationService.getInitialElo1vs1()); + players.put(spieler.getId(), spieler); + } + Stream results = ergebnis1vs1Repository.findAll().stream().sorted(new Ergebnis1vs1TimeComparator()); + results.forEach(r -> { + eloCalculationService.updateElo1vs1(players.get(r.getGewinner().getId()), + players.get(r.getVerlierer().getId()), + r.getToreVerlierer()); + }); + spielerRepository.saveAll(players.values()); + } + + /** + * Recalculate and overwrite all 2 vs 2 ELOs. + */ + public void recalculateAll2vs2() { + HashMap players = new HashMap<>(); + for (Spieler spieler : spielerRepository.findAll()) { + spieler.setElo2vs2(eloCalculationService.getInitialElo2vs2()); + players.put(spieler.getId(), spieler); + } + Stream results = ergebnis2vs2Repository.findAll().stream().sorted(new Ergebnis2vs2TimeComparator()); + results.forEach(r -> { + eloCalculationService.updateElo2vs2(players.get(r.getGewinnerVorn().getId()), + players.get(r.getGewinnerHinten().getId()), + players.get(r.getVerliererVorn().getId()), + players.get(r.getVerliererHinten().getId()), + r.getToreVerlierer()); + }); + spielerRepository.saveAll(players.values()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/Ergebnis1vs1TimeComparator.java b/src/main/java/org/kickerelo/kickerelo/util/Ergebnis1vs1TimeComparator.java new file mode 100644 index 0000000..660e0cd --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/Ergebnis1vs1TimeComparator.java @@ -0,0 +1,12 @@ +package org.kickerelo.kickerelo.util; + +import org.kickerelo.kickerelo.data.Ergebnis1vs1; + +import java.util.Comparator; + +public class Ergebnis1vs1TimeComparator implements Comparator { + @Override + public int compare(Ergebnis1vs1 o1, Ergebnis1vs1 o2) { + return o1.getTimestamp().compareTo(o2.getTimestamp()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/Ergebnis2vs2TimeComparator.java b/src/main/java/org/kickerelo/kickerelo/util/Ergebnis2vs2TimeComparator.java new file mode 100644 index 0000000..ed0ab69 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/Ergebnis2vs2TimeComparator.java @@ -0,0 +1,12 @@ +package org.kickerelo.kickerelo.util; + +import org.kickerelo.kickerelo.data.Ergebnis2vs2; + +import java.util.Comparator; + +public class Ergebnis2vs2TimeComparator implements Comparator { + @Override + public int compare(Ergebnis2vs2 o1, Ergebnis2vs2 o2) { + return o1.getTimestamp().compareTo(o2.getTimestamp()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/Spieler1vs1EloComparator.java b/src/main/java/org/kickerelo/kickerelo/util/Spieler1vs1EloComparator.java new file mode 100644 index 0000000..b424ffd --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/Spieler1vs1EloComparator.java @@ -0,0 +1,12 @@ +package org.kickerelo.kickerelo.util; + +import org.kickerelo.kickerelo.data.Spieler; + +import java.util.Comparator; + +public class Spieler1vs1EloComparator implements Comparator { + @Override + public int compare(Spieler o1, Spieler o2) { + return Float.compare(o2.getElo1vs1(), o1.getElo1vs1()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/Spieler2vs2EloComparator.java b/src/main/java/org/kickerelo/kickerelo/util/Spieler2vs2EloComparator.java new file mode 100644 index 0000000..f9ea739 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/Spieler2vs2EloComparator.java @@ -0,0 +1,12 @@ +package org.kickerelo.kickerelo.util; + +import org.kickerelo.kickerelo.data.Spieler; + +import java.util.Comparator; + +public class Spieler2vs2EloComparator implements Comparator { + @Override + public int compare(Spieler o1, Spieler o2) { + return Float.compare(o2.getElo2vs2(), o1.getElo2vs2()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/AdminView.java b/src/main/java/org/kickerelo/kickerelo/views/AdminView.java new file mode 100644 index 0000000..5098cbb --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/AdminView.java @@ -0,0 +1,52 @@ +package org.kickerelo.kickerelo.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.exception.DuplicatePlayerException; +import org.kickerelo.kickerelo.exception.InvalidDataException; +import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; +import org.kickerelo.kickerelo.service.KickerEloService; + +@Route("admin") +public class AdminView extends VerticalLayout { + public AdminView(KickerEloService service) { + H2 subheader = new H2("Verwaltung"); + + TextField spielername = new TextField("Spielername"); + spielername.addClassName("bordered"); + + // Button click listeners can be defined as lambda expressions + Button addPlayerButton = new Button("Spieler hinzufügen", e -> { + try { + service.addSpieler(spielername.getValue()); + } catch (PlayerNameNotSetException err) { + Notification.show("Spielername darf nicht leer sein").addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } catch (DuplicatePlayerException err) { + Notification.show("Spieler existiert bereits").addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } catch (InvalidDataException err) { + Notification.show("Name zu lang").addThemeVariants(NotificationVariant.LUMO_ERROR); + } + Notification.show("Spieler gespeichert").addThemeVariants(NotificationVariant.LUMO_SUCCESS); + }); + + Button recalc1vs1Button = new Button("1 vs 1 Elo neu berechnen", e -> { + Notification.show("Recalculating Elo").addThemeVariants(NotificationVariant.LUMO_WARNING); + service.recalculateAll1vs1(); + Notification.show("Recalculating finished").addThemeVariants(NotificationVariant.LUMO_SUCCESS); + }); + Button recalc2vs2Button = new Button("2 vs 2 Elo neu berechnen", e -> { + Notification.show("Recalculating Elo").addThemeVariants(NotificationVariant.LUMO_WARNING); + service.recalculateAll2vs2(); + Notification.show("Recalculating finished").addThemeVariants(NotificationVariant.LUMO_SUCCESS); + }); + + add(spielername, spielername, addPlayerButton, recalc1vs1Button, recalc2vs2Button); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/Chart1vs1.java b/src/main/java/org/kickerelo/kickerelo/views/Chart1vs1.java new file mode 100644 index 0000000..48e7df3 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/Chart1vs1.java @@ -0,0 +1,50 @@ +package org.kickerelo.kickerelo.views; + +import com.github.appreciated.apexcharts.ApexChartsBuilder; +import com.github.appreciated.apexcharts.config.Theme; +import com.github.appreciated.apexcharts.config.builder.ChartBuilder; +import com.github.appreciated.apexcharts.config.builder.XAxisBuilder; +import com.github.appreciated.apexcharts.config.builder.YAxisBuilder; +import com.github.appreciated.apexcharts.config.chart.Type; +import com.github.appreciated.apexcharts.config.chart.builder.ZoomBuilder; +import com.github.appreciated.apexcharts.config.chart.zoom.ZoomType; +import com.github.appreciated.apexcharts.config.theme.Mode; +import com.github.appreciated.apexcharts.config.theme.Monochrome; +import com.github.appreciated.apexcharts.config.xaxis.Labels; +import com.github.appreciated.apexcharts.config.xaxis.labels.Style; +import com.github.appreciated.apexcharts.config.yaxis.Title; +import com.github.appreciated.apexcharts.helper.Series; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.theme.lumo.Lumo; +import org.kickerelo.kickerelo.data.Spieler; +import org.kickerelo.kickerelo.util.Spieler1vs1EloComparator; + +import java.math.BigDecimal; +import java.util.List; + +public class Chart1vs1 extends ApexChartsBuilder { + public Chart1vs1(List l) { + Theme theme = new Theme(); + Monochrome monochrome = new Monochrome(); + monochrome.setEnabled(true); + theme.setMode(Mode.DARK); + theme.setMonochrome(monochrome); + Labels labels = new Labels(); + labels.setRotate(270d); + labels.setShow(true); + labels.setRotateAlways(false); + + + withChart(ChartBuilder.get().withType(Type.SCATTER) + .withZoom(ZoomBuilder.get().withEnabled(true).withType(ZoomType.XY).build()).build()) + .withSeries(new Series<>("ELO", + l.stream().sorted(new Spieler1vs1EloComparator()).map(Spieler::getElo1vs1).toArray() + )) + .withXaxis(XAxisBuilder.get().withCategories(l.stream().sorted(new Spieler1vs1EloComparator()) + .map(Spieler::getName).toList()).withLabels(labels).build()) + .withYaxis(YAxisBuilder.get().build()) + .withTheme(theme); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/Chart2vs2.java b/src/main/java/org/kickerelo/kickerelo/views/Chart2vs2.java new file mode 100644 index 0000000..696e0cd --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/Chart2vs2.java @@ -0,0 +1,41 @@ +package org.kickerelo.kickerelo.views; + +import com.github.appreciated.apexcharts.ApexChartsBuilder; +import com.github.appreciated.apexcharts.config.Theme; +import com.github.appreciated.apexcharts.config.builder.ChartBuilder; +import com.github.appreciated.apexcharts.config.builder.XAxisBuilder; +import com.github.appreciated.apexcharts.config.builder.YAxisBuilder; +import com.github.appreciated.apexcharts.config.chart.Type; +import com.github.appreciated.apexcharts.config.chart.builder.ZoomBuilder; +import com.github.appreciated.apexcharts.config.chart.zoom.ZoomType; +import com.github.appreciated.apexcharts.config.theme.Mode; +import com.github.appreciated.apexcharts.config.theme.Monochrome; +import com.github.appreciated.apexcharts.config.xaxis.Labels; +import com.github.appreciated.apexcharts.helper.Series; +import org.kickerelo.kickerelo.data.Spieler; +import org.kickerelo.kickerelo.util.Spieler2vs2EloComparator; + +import java.util.List; + +public class Chart2vs2 extends ApexChartsBuilder { + public Chart2vs2(List l) { + Theme theme = new Theme(); + Monochrome monochrome = new Monochrome(); + monochrome.setEnabled(true); + theme.setMode(Mode.DARK); + theme.setMonochrome(monochrome); + Labels labels = new Labels(); + labels.setRotate(270d); + labels.setShow(true); + labels.setRotateAlways(false); + + withChart(ChartBuilder.get().withType(Type.SCATTER) + .withZoom(ZoomBuilder.get().withEnabled(true).withType(ZoomType.XY).build()).build()) + .withSeries(new Series<>("ELO", + l.stream().sorted(new Spieler2vs2EloComparator()).map(Spieler::getElo2vs2).toArray() + )) + .withXaxis(XAxisBuilder.get().withCategories(l.stream().sorted(new Spieler2vs2EloComparator()).map(Spieler::getName).toList()).withLabels(labels).build()) + .withYaxis(YAxisBuilder.get().build()) + .withTheme(theme); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/MainView.java b/src/main/java/org/kickerelo/kickerelo/views/Enter1vs1View.java similarity index 51% rename from src/main/java/org/kickerelo/kickerelo/MainView.java rename to src/main/java/org/kickerelo/kickerelo/views/Enter1vs1View.java index b9d0570..53745c9 100644 --- a/src/main/java/org/kickerelo/kickerelo/MainView.java +++ b/src/main/java/org/kickerelo/kickerelo/views/Enter1vs1View.java @@ -1,47 +1,24 @@ -package org.kickerelo.kickerelo; +package org.kickerelo.kickerelo.views; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.IntegerField; -import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.exception.DuplicatePlayerException; +import org.kickerelo.kickerelo.exception.InvalidDataException; +import org.kickerelo.kickerelo.exception.NoSuchPlayerException; +import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; +import org.kickerelo.kickerelo.service.KickerEloService; -/** - * A sample Vaadin view class. - *

- * To implement a Vaadin view just extend any Vaadin component and use @Route - * annotation to announce it in a URL as a Spring managed bean. - *

- * A new instance of this class is created for every new user and every browser - * tab/window. - *

- * The main view contains a text field for getting the user name and a button - * that shows a greeting message in a notification. - */ -@Route -public class MainView extends VerticalLayout { +@Route(value = "enter1vs1") +public class Enter1vs1View extends VerticalLayout { - /** - * Construct a new Vaadin view. - */ - - public MainView(KickerEloService eloService) { - - - TextField spielername = new TextField("Spielername"); - spielername.addClassName("bordered"); - - // Button click listeners can be defined as lambda expressions - Button button = new Button("Spieler hinzufügen", e -> { - eloService.addSpieler(spielername.getValue()); - Notification.show("Spieler gespeichert").addThemeVariants(NotificationVariant.LUMO_SUCCESS); - }); - - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + public Enter1vs1View(KickerEloService eloService) { + H2 subheading = new H2("1 vs 1 Ergebnis"); ComboBox winnerSelect = new ComboBox<>("Gewinner"); winnerSelect.setItems(eloService.getSpielerNamen()); @@ -62,7 +39,13 @@ public class MainView extends VerticalLayout { eloService.enterResult1vs1(winnerSelect.getValue(), loserSelect.getValue(), loserGoals.getValue().shortValue()); Notification.show("Gespeichert").addThemeVariants(NotificationVariant.LUMO_SUCCESS); } catch (NoSuchPlayerException err) { - Notification.show("Konnte nicht gespeichert werden").addThemeVariants(NotificationVariant.LUMO_ERROR); + Notification.show("Unbekannter Spieler").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (DuplicatePlayerException err) { + Notification.show("Alle Spieler müssen paarweise verschieden sein").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (PlayerNameNotSetException err) { + Notification.show("Alle Spieler müssen gesetzt sein").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (InvalidDataException err) { + Notification.show("Verliertore falsch").addThemeVariants(NotificationVariant.LUMO_ERROR); } }); @@ -72,6 +55,6 @@ public class MainView extends VerticalLayout { // styles.css. addClassName("centered-content"); - add(spielername, button, winnerSelect, loserSelect, loserGoals, saveButton); + add(subheading, winnerSelect, loserSelect, loserGoals, saveButton); } -} \ No newline at end of file +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/Enter2vs2View.java b/src/main/java/org/kickerelo/kickerelo/views/Enter2vs2View.java new file mode 100644 index 0000000..e84fa66 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/Enter2vs2View.java @@ -0,0 +1,66 @@ +package org.kickerelo.kickerelo.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.exception.DuplicatePlayerException; +import org.kickerelo.kickerelo.exception.InvalidDataException; +import org.kickerelo.kickerelo.exception.NoSuchPlayerException; +import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; +import org.kickerelo.kickerelo.service.KickerEloService; + +@Route(value = "enter2vs2") +public class Enter2vs2View extends VerticalLayout { + public Enter2vs2View(KickerEloService eloService) { + H2 subheading = new H2("2 vs 2 Ergebnis"); + + ComboBox winnerFrontSelect = new ComboBox<>("Gewinner vorne"); + winnerFrontSelect.setItems(eloService.getSpielerNamen()); + winnerFrontSelect.setPlaceholder("Spieler auswählen"); + + ComboBox winnerBackSelect = new ComboBox<>("Gewinner hinten"); + winnerBackSelect.setItems(eloService.getSpielerNamen()); + winnerBackSelect.setPlaceholder("Spieler auswählen"); + + ComboBox loserFrontSelect = new ComboBox<>("Verlierer vorne"); + loserFrontSelect.setItems(eloService.getSpielerNamen()); + loserFrontSelect.setPlaceholder("Spieler auswählen"); + + ComboBox loserBackSelect = new ComboBox<>("Verlierer hinten"); + loserBackSelect.setItems(eloService.getSpielerNamen()); + loserBackSelect.setPlaceholder("Spieler auswählen"); + + IntegerField loserGoals = new IntegerField("Tore des Verlierers"); + loserGoals.setMin(0); + loserGoals.setMax(9); + loserGoals.setValue(0); + loserGoals.setStepButtonsVisible(true); + + Button saveButton = new Button("Speichern", e -> { + try { + eloService.enterResult2vs2(winnerFrontSelect.getValue(), winnerBackSelect.getValue(), loserFrontSelect.getValue(), loserBackSelect.getValue(), loserGoals.getValue().shortValue()); + Notification.show("Gespeichert").addThemeVariants(NotificationVariant.LUMO_SUCCESS); + } catch (NoSuchPlayerException err) { + Notification.show("Unbekannter Spieler").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (DuplicatePlayerException err) { + Notification.show("Alle Spieler müssen paarweise verschieden sein").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (PlayerNameNotSetException err) { + Notification.show("Alle Spieler müssen gesetzt sein").addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (InvalidDataException err) { + Notification.show("Verliertore falsch").addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }); + + + // Use custom CSS classes to apply styling. This is defined in + // styles.css. + addClassName("centered-content"); + + add(subheading, winnerFrontSelect, winnerBackSelect, loserFrontSelect, loserBackSelect, loserGoals, saveButton); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/Graph1vs1View.java b/src/main/java/org/kickerelo/kickerelo/views/Graph1vs1View.java new file mode 100644 index 0000000..7f216e4 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/Graph1vs1View.java @@ -0,0 +1,20 @@ +package org.kickerelo.kickerelo.views; + +import com.github.appreciated.apexcharts.ApexChartsBuilder; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.service.KickerEloService; +import org.springframework.beans.factory.annotation.Autowired; + +@Route("graph1vs1") +public class Graph1vs1View extends VerticalLayout { + + ApexChartsBuilder chart1vs1; + public Graph1vs1View(KickerEloService service) { + H2 subheading = new H2("1 vs 1 Elo"); + chart1vs1 = new Chart1vs1(service.getSpielerEntities()); + add(subheading, chart1vs1.build()); + } + +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/Graph2vs2View.java b/src/main/java/org/kickerelo/kickerelo/views/Graph2vs2View.java new file mode 100644 index 0000000..56353ca --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/Graph2vs2View.java @@ -0,0 +1,17 @@ +package org.kickerelo.kickerelo.views; + +import com.github.appreciated.apexcharts.ApexChartsBuilder; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.service.KickerEloService; + +@Route("graph2vs2") +public class Graph2vs2View extends VerticalLayout { + ApexChartsBuilder chart2vs2; + public Graph2vs2View(KickerEloService service) { + H2 subheading = new H2("2 vs 2 Elo"); + chart2vs2 = new Chart2vs2(service.getSpielerEntities()); + add(subheading, chart2vs2.build()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/PlayerListView.java b/src/main/java/org/kickerelo/kickerelo/views/PlayerListView.java new file mode 100644 index 0000000..4be1c89 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/views/PlayerListView.java @@ -0,0 +1,38 @@ +package org.kickerelo.kickerelo.views; + +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.data.provider.SortDirection; +import com.vaadin.flow.router.Route; +import org.kickerelo.kickerelo.data.Spieler; +import org.kickerelo.kickerelo.service.KickerEloService; + +import java.util.List; + +@Route("") +public class PlayerListView extends VerticalLayout { + public PlayerListView(KickerEloService eloService) { + setSizeFull(); + H2 subheading = new H2("Spielerliste"); + + List players = eloService.getSpielerEntities(); + Grid playerGrid = new Grid<>(Spieler.class); + + playerGrid.setItems(players); + playerGrid.removeColumnByKey("id"); + playerGrid.removeColumnByKey("elo_alt"); + Grid.Column nameColumn = playerGrid.getColumnByKey("name"); + Grid.Column elo1vs1Column = playerGrid.getColumnByKey("elo1vs1"); + Grid.Column elo2vs2Column = playerGrid.getColumnByKey("elo2vs2"); + nameColumn.setHeader("Name"); + elo1vs1Column.setHeader("Elo 1 vs 1"); + + playerGrid.setColumnOrder(nameColumn, elo1vs1Column, elo2vs2Column); + + GridSortOrder sortOrder = new GridSortOrder<>(elo1vs1Column, SortDirection.DESCENDING); + playerGrid.sort(List.of(sortOrder)); + add(subheading, playerGrid); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 743bb0a..902fa33 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,10 +6,9 @@ spring.mustache.check-template-location = false vaadin.launch-browser=true spring.datasource.url=jdbc:mariadb://localhost:3306/kickerelo -spring.datasource.username= -spring.datasource.password= +spring.datasource.username=root +spring.datasource.password=root spring.datasource.driver-class-name=org.mariadb.jdbc.Driver spring.jpa.hibernate.ddl-auto=validate -spring.jpa.show-sql=true -spring.jpa.format-sql=true +spring.jpa.show-sql=false spring.jpa.open-in-view=false \ No newline at end of file