From: Ole B. Rosentreter Date: Wed, 16 Jul 2025 08:51:24 +0000 (+0200) Subject: version 1.0 X-Git-Url: https://git.laktatnebel.de/?a=commitdiff_plain;h=0c9db7f882a84e44357c82a5cd52c7edf426d7cf;p=roadtokona.triathlon-coaching.com.git version 1.0 --- diff --git a/roadtokona-db/sql/schema.sql b/roadtokona-db/sql/schema.sql index 102a4bf..2cd89fb 100644 --- a/roadtokona-db/sql/schema.sql +++ b/roadtokona-db/sql/schema.sql @@ -32,6 +32,8 @@ CREATE TABLE roadtokona.result ( CREATE VIEW roadtokona.view_result_normalized AS SELECT + fk_race, + fk_division, race_date, race_location, division_gender, @@ -42,17 +44,62 @@ SELECT FROM roadtokona.result re JOIN roadtokona.race ra ON (re.fk_race = ra.race_id) -JOIN roadtokona.division di ON (re.fk_division = di.division_id); +JOIN roadtokona.division di ON (re.fk_division = di.division_id) +ORDER BY + result_normalized; CREATE VIEW roadtokona.view_division_factor AS SELECT - division_gender, - division_age, - factor_value + factor_value, + division_gender || division_age FROM roadtokona.factor fa JOIN roadtokona.division di ON (fa.fk_division = di.division_id); +CREATE VIEW roadtokona.view_division AS +SELECT + division_gender || division_age as agegroup, + division_gender || division_age +FROM + roadtokona.factor fa +JOIN roadtokona.division di ON (fa.fk_division = di.division_id); + +CREATE VIEW roadtokona.view_division_winner AS +SELECT + min(result_finish) as finish_winner, + fk_division, + fk_race +FROM + roadtokona.result +GROUP BY + fk_division, + fk_race; + +CREATE VIEW roadtokona.view_race AS +SELECT + race_id, + race_location || ' (' || to_char(race_date, 'DD.MM.YYYY') || ')' +FROM + roadtokona.race +ORDER BY + race_date; + +CREATE VIEW roadtokona.view_result_normalized_rolldown AS +SELECT + roadtokona.view_result_normalized.* +FROM + roadtokona.view_result_normalized +WHERE NOT EXISTS ( + SELECT 1 + FROM roadtokona.view_division_winner + WHERE roadtokona.view_result_normalized.result_finish = roadtokona.view_division_winner.finish_winner + AND roadtokona.view_result_normalized.fk_division = roadtokona.view_division_winner.fk_division + AND roadtokona.view_result_normalized.fk_race = roadtokona.view_division_winner.fk_race +) +ORDER BY + result_normalized; + + insert into roadtokona.division values (default, 'M', 18), (default, 'F', 18), @@ -121,6 +168,19 @@ insert into roadtokona.factor values --ALTER TABLE roadtokona.result OWNER TO laktatnebel; --ALTER TABLE roadtokona.view_result_normalized OWNER TO laktatnebel; ---GRANT ALL ON SCHEMA roadtokona TO oleb; +ALTER SCHEMA roadtokona OWNER TO oleb; + +ALTER TABLE roadtokona.division OWNER TO oleb; +ALTER TABLE roadtokona.race OWNER TO oleb; +ALTER TABLE roadtokona.factor OWNER TO oleb; +ALTER TABLE roadtokona.result OWNER TO oleb; +ALTER TABLE roadtokona.view_result_normalized OWNER TO oleb; +ALTER TABLE roadtokona.view_division OWNER TO oleb; +ALTER TABLE roadtokona.view_division_factor OWNER TO oleb; +ALTER TABLE roadtokona.view_race OWNER TO oleb; +ALTER TABLE roadtokona.view_division_winner OWNER TO oleb; +ALTER TABLE roadtokona.view_result_normalized_rolldown OWNER TO oleb; + +GRANT ALL ON SCHEMA roadtokona TO oleb; diff --git a/roadtokona-web/src/webui/getRaceData.php b/roadtokona-web/src/webui/getRaceData.php new file mode 100644 index 0000000..e6d9818 --- /dev/null +++ b/roadtokona-web/src/webui/getRaceData.php @@ -0,0 +1,46 @@ + + + + 'Serverfehler', + 'message' => $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/roadtokona-web/src/webui/index.php b/roadtokona-web/src/webui/index.php new file mode 100755 index 0000000..e78a337 --- /dev/null +++ b/roadtokona-web/src/webui/index.php @@ -0,0 +1,123 @@ + + + + + + + + + + + + + +roadtokona.triathlon-coaching.com + + + + + +
+

#roadtokona

+
+ +
+

Was ist neu?

+

Der Weg nach Kona ist anders geworden. Laut IRONMAN Cooperation fairer, aber definitiv weniger transparent.

+

Bislang reichte ein bestimmter Platz in deienr Altersklasse für einen Slot, oder man konnte auf das Roll-down-verfahren hoffen und nachrücken.
+Das war auch für den Support / Coach einfach: Am Live-Tracker war der aktuelle Stand der Dinge jederzeit ersichtlich.
"Platz 5 ist 2 Minuten vor Dir, gib Gas!" - diese Rufe hört man nun selten.

+

Jetzt werden alle Zeiten im Ziel erst mit einem für jede Altersklasse spezifischen Faktor multipliziert und daraus eine Liste mit "normalisierten Zeiten" generiert.
Anhand derer werden dann die begehrten Slots vergeben.

+

Ganz einfach, oder? Profis und PC/HC haben eigene Verfahren und die Ersten der jeweiligen Altersklassen sind auf jeden Fall qualifiziert.

+ + +

Faktoren

+

Diese Faktoren hat die IRONMAN Cooperation veröffentlicht.
+Da diese auf echten Ergebnissen beruhen, können sie sich jede Sasion auch ändern - ich werde hier immer nur die allerneusten Werte verwenden.

+
+

+

\n

\n"; +// echo "
"; + } else { + echo "
\n"; + } +} +?> +* Mangels Daten in den Klassen F80 und älter gibt es dafür keine Faktoren. +

+ +

Wie kann ich meine Zeit umrechnen?

+
+
+Rechner +Deine Zeit: + +
Deine Altersklasse: + +
+

Ergebnis: 00:00:00

+
+
+
+ +

Wie schnell hätte ich sein müssen?

+

Das ist die Gretchenfrage. :-) Da die Slotvergabe nach dem neuen System erst ab dem 16. August 2025 greift, sind die Ergebnisse aller anderen Wettkämpfe davor nur bedingt aussagekräftig.
+Aber Du kannst ja gucken, welche Zeit in welcher Klasse zu welcher Platzierung in der "normalisierten Liste" gereicht hätte.
Die Schlüsse daraus kannst Du ja selber ziehen ;-)

+

Du kannst den Wettkampf auswählen und bekommst die Rolldown-Liste. Das heißt die Liste ohne Profis, PC/HC und Ersten der jeweiligen Altersklasse.
+Wenn es beispielsweise 75 Slots für die begehrte Insel gibt, sind davon schon maximal 26 durch die Ersten belegt. Bleiben noch 49.

+
+
+Wettkampfauswahl +wähle aus: +
... und markiere eine Altersklasse: + +
+

Bitte Wettkampf auswählen.

+
+
+
+ +
+ + + + + + + diff --git a/roadtokona-web/src/webui/js/roadtokona.js b/roadtokona-web/src/webui/js/roadtokona.js new file mode 100644 index 0000000..65b3fda --- /dev/null +++ b/roadtokona-web/src/webui/js/roadtokona.js @@ -0,0 +1,164 @@ +/** + * + */ + +document.addEventListener('DOMContentLoaded', function() { + // Elemente auswählen + const hoursSelect = document.getElementById('yourhour'); + const minutesSelect = document.getElementById('yourminute'); + const secondsSelect = document.getElementById('yoursecond'); + const factorSelect = document.getElementById('yourag'); + const resultParagraph = document.getElementById('result'); + + // Event-Listener für alle Auswahllisten + [hoursSelect, minutesSelect, secondsSelect, factorSelect].forEach(select => { + select.addEventListener('change', calculateResult); + }); + + // Funktion zur Formatierung der Zeit im hh:mm:ss Format + function formatTime(totalSeconds) { + // Sicherstellen, dass wir mit einer ganzen Zahl arbeiten + totalSeconds = Math.round(totalSeconds); + + // Stunden, Minuten und Sekunden berechnen + const hours = Math.floor(totalSeconds / 3600); + const remainingSeconds = totalSeconds % 3600; + const minutes = Math.floor(remainingSeconds / 60); + const seconds = remainingSeconds % 60; + + // Führende Nullen hinzufügen + const pad = num => num.toString().padStart(2, '0'); + + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; + } + + // Funktion zur Berechnung des Ergebnisses + function calculateResult() { + // Werte auslesen + const hours = parseInt(hoursSelect.value) || 0; + const minutes = parseInt(minutesSelect.value) || 0; + const seconds = parseInt(secondsSelect.value) || 0; + const factor = parseFloat(factorSelect.value) || 1; + + // Gesamtzeit in Sekunden berechnen + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + + // Ergebnis berechnen + const result = totalSeconds * factor; + + // Ergebnis formatieren und anzeigen + resultParagraph.textContent = `Ergebnis: ${formatTime(result)}`; + } + + // Initiale Berechnung + calculateResult(); + }); + + + +document.getElementById('race').addEventListener('change', function() { + const selection = this.value; + const resultsDiv = document.getElementById('results'); + + if (!selection) { + resultsDiv.innerHTML = '

Bitte Wettkampf auswählen.

'; + return; + } + + resultsDiv.innerHTML = '

Daten werden geladen...

'; + + // AJAX-Anfrage an den Server + fetch(`getRaceData.php?rid=${encodeURIComponent(selection)}`) + .then(response => { + if (!response.ok) { + throw new Error(`Serverfehler: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (!data || data.length === 0) { + resultsDiv.innerHTML = '

Keine Daten gefunden.

'; + return; + } + + // Tabelle erstellen + let html = '

OMG - ist das flott!
Brauchst Du einen Coach?

' + html += ''; + + // Spaltenüberschriften + const columns = Object.keys(data[0]); + columns.forEach(col => { + html += ``; + }); + html += ''; + + ct=0; + // Datenzeilen + data.forEach(row => { + ct++; + html += ''; + html += ''; + columns.forEach(col => { + html += ``; + }); + html += ''; + }); + + html += '
#${col}
'+ct+'${row[col] ?? ''}
'; + resultsDiv.innerHTML = html; + // Nachdem die Tabelle geladen wurde, den Event-Listener für die Altersklassenauswahl setzen + setupAgeGroupHighlighting(); + }) + .catch(error => { + resultsDiv.innerHTML = `

Fehler beim Laden der Daten: ${error.message}

`; + console.error('Fehler:', error); + }); +}); + + + + +// Funktion für die Markierung der Altersklassen +function setupAgeGroupHighlighting() { + const auswahl = document.getElementById('ag'); + const tabelle = document.getElementById('resultlist'); + + // Event-Listener entfernen, falls bereits vorhanden + auswahl.removeEventListener('change', handleAgeGroupSelection); + + // Neuen Event-Listener hinzufügen + auswahl.addEventListener('change', handleAgeGroupSelection); +} + +// Handler für die Altersklassenauswahl +function handleAgeGroupSelection() { + const tabelle = document.getElementById('resultlist'); + if (!tabelle) return; + + // Zuerst alle Markierungen entfernen + const bereitsMarkiert = tabelle.querySelectorAll('.highlight'); + bereitsMarkiert.forEach(element => { + element.classList.remove('highlight'); + }); + + // Wenn eine Option ausgewählt wurde (nicht die leere Standardoption) + if (this.value) { + const selectedText = this.options[this.selectedIndex].text; + console.log('selectedText:', selectedText); + + const zellen = tabelle.querySelectorAll('td'); + zellen.forEach(zelle => { + if (zelle.textContent === selectedText) { + zelle.closest('tr').classList.add('highlight'); + } + }); + } +} + +// Initialen Event-Listener setzen (falls Tabelle bereits existiert) +document.addEventListener('DOMContentLoaded', function() { + // Prüfen, ob die Tabelle bereits existiert (z.B. bei Seitenrefresh) + if (document.getElementById('resultlist')) { + setupAgeGroupHighlighting(); + } +}); \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/database_functions.php b/roadtokona-web/src/webui/lib/database_functions.php new file mode 100644 index 0000000..769cd86 --- /dev/null +++ b/roadtokona-web/src/webui/lib/database_functions.php @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/db/database_functions_pgsql.php b/roadtokona-web/src/webui/lib/db/database_functions_pgsql.php new file mode 100644 index 0000000..7d3ca8e --- /dev/null +++ b/roadtokona-web/src/webui/lib/db/database_functions_pgsql.php @@ -0,0 +1,104 @@ +".$res_sql_result; + $return_bool_value = true; + } else { + return "Datenbankabfrage fehlgeschlagen!"; + } + + return $return_bool_value; +} + + +// SQL an DB absetzen +// Parameter: Tablle, Feld(er), Wert(e) +// Rückgabewert: Array +function getData ($dbms_connection, $db_query) { + $return_arr_data = array(); // Rückgabewert als Array + //echo "\n

SQL:
".$ref_str_db_query."

"; + + // DB Abfage starten + //echo $db_query; + $res_sql_result = pg_query ($dbms_connection, $db_query); + // Gültigkeit der DB Abfage testen + if ($res_sql_result) { + //echo "

".$res_sql_result."

"; + while ($arr_sql_data = pg_fetch_row($res_sql_result)) { + //echo debugPrint($arr_sql_data); + array_push ($return_arr_data, $arr_sql_data); + } + //var_dump( $return_arr_data); + } else { + return "Datenbankabfrage fehlgeschlagen!"; + } + + return $return_arr_data; +} + + +// SQL an DB absetzen +// Parameter: Tablle, Feld(er), Wert(e) +// Rückgabewert: Array +function getDataReturnID ($dbms_connection, $db_query) { + $return_arr_data = array(); // Rückgabewert als Array + //echo "\n

SQL:
".$ref_str_db_query."

"; + + // DB Abfage starten + //echo $db_query; + $res_sql_result = pg_query ($dbms_connection, $db_query); + // Gültigkeit der DB Abfage testen + if ($res_sql_result) { + //echo "

".$res_sql_result."

"; + while ($arr_sql_data = pg_fetch_row($res_sql_result)) { + //echo debugPrint($arr_sql_data); + array_push ($return_arr_data, $arr_sql_data); + } + //var_dump( $return_arr_data); + } else { + return "Datenbankabfrage fehlgeschlagen!"; + } + + return $return_arr_data[0][0]; +} + + +?> \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/db/database_functions_select.php b/roadtokona-web/src/webui/lib/db/database_functions_select.php new file mode 100644 index 0000000..fa8e18a --- /dev/null +++ b/roadtokona-web/src/webui/lib/db/database_functions_select.php @@ -0,0 +1,78 @@ + \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/gui/gui_functions_select.php b/roadtokona-web/src/webui/lib/gui/gui_functions_select.php new file mode 100644 index 0000000..6a6c248 --- /dev/null +++ b/roadtokona-web/src/webui/lib/gui/gui_functions_select.php @@ -0,0 +1,95 @@ + + * $ref_value_array Daten-Array; null möglich + * $ref_id Id des ; null möglich + * $ref_tabindex Tabindex in der
+ + * $ref_start Start-Index statt Datenarray falls Datenarray null + * $ref_end End-Index statt Datenarray falls Datenarray null + * $ref_int_preselect Index vorausgewähltes Element; default -1 + * $ref_multiple Mehrfachauswahl stehen soll + * $ref_intend Anz. Tabs einrücken + * */ +function makeSelect($ref_select_name, $ref_value_array, $ref_id, $ref_class, $ref_tabindex, $ref_start, $ref_end, $ref_int_preselect, $ref_multiple, $ref_int_size, $ref_javascript, $ref_mandantory, $ref_labeltitle, $ref_p_flag, $ref_newline_flag, $ref_intend) { + + $intend = make_intend_str($ref_intend); + + if ($ref_p_flag) { + echo $intend; + echo "

\n"; + } + + echo $intend."\t"; + + if ($ref_mandantory) { + echo "* "; + } + + if ($ref_labeltitle != null) { + echo ""; + if ($ref_newline_flag) { + echo "
"; + } + echo "\n"; + } + + $str_id = $ref_id == null ? " " : " id=\"".$ref_id."\" "; + $str_class = $ref_class == null ? " " : " class=\"".$ref_class."\" "; + $str_tabindex = $ref_tabindex == null ? " " : " tabindex=\"".$ref_tabindex."\" "; + $str_javascript = $ref_javascript == null ? " " : " onchange=\"".$ref_javascript."\" "; + + echo $intend."\t"; + + echo "\n"; + + if ($ref_p_flag) { + echo $intend; + echo "

\n"; + } +} + + + + + +?> \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/gui_functions.php b/roadtokona-web/src/webui/lib/gui_functions.php new file mode 100644 index 0000000..fd747a2 --- /dev/null +++ b/roadtokona-web/src/webui/lib/gui_functions.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/util/util_functions.php b/roadtokona-web/src/webui/lib/util/util_functions.php new file mode 100644 index 0000000..f0e70c7 --- /dev/null +++ b/roadtokona-web/src/webui/lib/util/util_functions.php @@ -0,0 +1,65 @@ +"; + if (is_array($param)) { + if ($asList) { + debugPrintAsList($param); + } else { + echo "_"; + foreach ($param as $p) { + echo $p."_"; + } + } + } else { + echo $param; + } + echo "

\n"; +} + +function debugPrintAsList($param) { + echo "
    \n"; + foreach ($param as $p) { + echo "
  1. ".$p."_
  2. \n"; + } + echo "
\n"; +} + +function coordinates ($ref_longitude, $ref_latitude) { + $return_geo = array("",""); + + $return_geo[0] = $ref_longitude[0] + $ref_longitude[1]/60 + $ref_longitude[2]/3600; + $return_geo[1] = $ref_latitude[0] + $ref_latitude[1]/60 + $ref_latitude[2]/3600; + + return $return_geo; +} + +function find_pos_in_array($ref_array, $ref_value) { + $pos = -1; + + for ($i=0; $i \ No newline at end of file diff --git a/roadtokona-web/src/webui/lib/util_functions.php b/roadtokona-web/src/webui/lib/util_functions.php new file mode 100644 index 0000000..63b5bd3 --- /dev/null +++ b/roadtokona-web/src/webui/lib/util_functions.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/roadtokona-web/src/webui/stylesheet/roadtokona.css b/roadtokona-web/src/webui/stylesheet/roadtokona.css new file mode 100644 index 0000000..a344930 --- /dev/null +++ b/roadtokona-web/src/webui/stylesheet/roadtokona.css @@ -0,0 +1,105 @@ + +BODY { + font-size: 100.01%; + font-family: Verdana, sans-serif; + padding: 0; + margin: 0; +} + +TABLE, TH, TD { + border: none; +} + +TH, TD { + padding: 5px; + text-align: right; +} + +H1 { + font-size: 1.5em; + margin: 0 0 0.5em 1em; + color: rgb(0, 121, 138); +} + +H2 { + font-size: 1.25em; + margin: 1em 0 0.5em 1em; + color: rgb(0, 121, 138); +} + +P { + margin: 0 2em; +} + +P.cta, +P.cta A { + margin: 0 2em; + color: rgb(166, 12, 36); + font-weight: 600; +} + +FORM, SELECT, OPTION { + font-size: 1.150em; +} + +SELECT, OPTION { + background-color: rgb(243, 173, 0); +} + +FORM { + margin: 0 2em; + background-color: rgb(173, 218, 213); +} + +LEGEND { + font-weight: bold; +} + +.factors-container { + display: flex; + gap: 1rem; + margin: 0 2em; + margin-top: 1rem; + /*--font-size: 250%;*/ +} + +.factors-container div { + flex: 1; + padding: 1rem; + border: 1px solid #000; + background-color: rgb(226, 239, 243); +} + +/* Mobile: Untereinander */ +@media (max-width: 480px) { + .factors-container { + flex-direction: column; + gap: 1rem; + } +} + +#results, +#result { + border: 1px solid #000; + background-color: rgb(226, 239, 243); + margin-top: 1rem; +} + +.highlight { + background-color: yellow; + font-weight: bold; +} + +FOOTER { + margin: 1rem 0 1em 1em; +} + +A { + color: #000; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +}