diff --git a/resources/qml/Components/SpeedFlowChart.js b/resources/qml/Components/SpeedFlowChart.js new file mode 100644 index 0000000..7644ba8 --- /dev/null +++ b/resources/qml/Components/SpeedFlowChart.js @@ -0,0 +1,289 @@ +.pragma library + +/** + * Function to extract results from PartiipantFromApi + * @param {ParticipantFromApi} fromApi + * @return {Result[]} + */ +function _extractResults(fromApi) { + const results = Array(7); + + const existingResults = Object.keys(fromApi).filter(key => + key.match(/result[0-9]/), + ); + + for (const result of existingResults) { + let roundNumber = 0; + const match = result.match(/result([0-9])/); + if (match !== undefined && match !== null) { + roundNumber = parseInt(match[1]); + } + if (roundNumber < 0) { + continue; + } + results[roundNumber] = { + rank: parseInt(fromApi[`result_rank${roundNumber}`] as string), + result: fromApi[`result${roundNumber}`] as string, + }; + } + return results; +} + +/** + * Function to clean up participants from the api + * @param {ParticipantFromApi} fromApi + * @return {Participant} + */ +function participantFromApiParticipant( + fromApi, +) { + const results = _extractResults(fromApi); + + return { + id: fromApi.PerId, + firstName: fromApi.firstname, + lastName: fromApi.lastname, + results: results, + overallRank: fromApi.result_rank, + startNumber: parseInt(fromApi.start_number), + }; +} + + +/** + * + * @param {number} roundNumber + * @param {RouteNames} routeNames + * @return {string | undefined} + */ +function getRoundName( + roundNumber, + routeNames, +) { + if (roundNumber < 2 || roundNumber > 6) return undefined; + + return routeNames[roundNumber]; +} + +/** + * + * @param {string} name + * @return {number} + */ +function getRoundRank(name) { + const match = name.match(/1\/([842])/); + if (match === undefined || match === null || match.length !== 2) { + return 2; + } + return parseInt(match[1]); +} + +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundIndex + */ +function computeWinnerOfPair(pair, roundIndex) { + if ( + !(pair.laneA && pair.laneA.participant.results[roundIndex] && pair.laneA.participant.results[roundIndex].rank) || + !(pair.laneB && pair.laneB.participant.results[roundIndex] && pair.laneB.participant.results[roundIndex].rank) + ) + return; + + pair.laneA.result = pair.laneA.participant.results[roundIndex]; + pair.laneB.result = pair.laneB.participant.results[roundIndex]; + + if (pair.winner === undefined) { + pair.winner = + pair.laneA.participant.results[roundIndex].rank > + pair.laneB.participant.results[roundIndex].rank + ? 'B' + : 'A'; + } +} + +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundNumber + * @return {Participant | undefined} + */ +function getWinnerOfPair( + pair, + roundNumber, +) { + computeWinnerOfPair(pair, roundNumber); + return { + ['A']: pair.laneA ? pair.laneA.participant : undefined, + ['B']: pair.laneB ? pair.laneB.participant : undefined, + ['']: undefined, + }[pair.winner ? pair.winner:'']; +} + +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundNumber + * @return {Participant | undefined} + */ +function getLooserOfPair( + pair, + roundNumber, +) { + computeWinnerOfPair(pair, roundNumber); + return { + ['A']: pair.laneB ? pair.laneB.participant : undefined, + ['B']: pair.laneA ? pair.laneA.participant : undefined, + ['']: undefined, + }[pair.winner ? pair.winner:'']; +} + +/** + * + * @param {number} roundIndex index of the new round + * @param {string} roundName name of the new round + * @param {SpeedRound} previousRound + * @param {number} roundRank + * @param {boolean} takeLooser + * @return {SpeedRound} + */ +function computeRoundFromPreviousRound( + roundIndex, + roundName, + previousRound, + roundRank, + takeLooser = false, +) { + const getAdvancingParticipant = takeLooser + ? getLooserOfPair + : getWinnerOfPair; + const nextRoundPairs = new Array(roundRank / 2).fill(0).map((_, i) => { + const laneAParticipant = getAdvancingParticipant( + previousRound.pairs[i * 2], + previousRound.roundIndex, + ); + const laneBParticipant = getAdvancingParticipant( + previousRound.pairs[i * 2 + 1], + previousRound.roundIndex, + ); + + return { + laneA: + laneAParticipant === undefined + ? undefined + : { + participant: laneAParticipant, + }, + laneB: + laneBParticipant === undefined + ? undefined + : { + participant: laneBParticipant, + }, + }; + }); + + return { + pairs: nextRoundPairs, + roundIndex: roundIndex, + roundName: roundName, + }; +} + +/** + * + * @param {SpeedCompetitionCategoryResult} result The result to process + * @return {SpeedFlowchartResult} + */ +function convertResultsToSpeedFlowchartResult( + result, +) { + const rounds = []; + const convertedParticipants = result.participants + .map(fromApi => participantFromApiParticipant(fromApi)) + // sort by qualification result + .sort((a, b) => a.results[0].rank - b.results[0].rank); + + const roundIndices = Object.keys(result.route_names) + .map(number => parseInt(number)) + .filter(number => number > 0); + + // process first round + const firstRoundName = getRoundName(roundIndices[0], result.route_names); + const tmpRoundName = getRoundName(roundIndices[0], result.route_names); + const firstRoundRank = getRoundRank( + tmpRoundName ? tmpRoundName:'', + ); + + const getOpponent = (ofRank) => { + return convertedParticipants[firstRoundRank * 2 - 1 - ofRank]; + }; + + // Should be: + // 0, 1, 2, 3, 4, 5, 6, 7 + // - 1,16, 8, 9, 4,13, 5,12, 2,15, 7,10, 3,14, 6,11 + // - 1, 8, 4, 5, 2, 7, 3, 6 for firstRoundNumber=8 + // - 1, 4, 2, 3 for firstRoundNumber=4 + // - 1, 2 for firstRoundNumber=2 + // TODO: come up with a proper alogorithm maybe + const ranksOfLaneAInOrder = [ + [1, 2], + [1, 4, 2, 3], + [1, 8, 4, 5, 2, 7, 3, 6], + ][Math.floor(firstRoundRank / 4)]; + + const firstRoundPairs = ranksOfLaneAInOrder.map(rank => { + return { + laneA: { participant: convertedParticipants[rank - 1] }, + laneB: { participant: getOpponent(rank - 1) }, + }; + }); + + const firstRound = { + pairs: firstRoundPairs, + roundIndex: roundIndices[0], + roundName: firstRoundName, + }; + + rounds.push(firstRound); + + // compute following rounds + let roundIndex = roundIndices[1]; + for (let roundRank = firstRoundRank; roundRank > 2; roundRank /= 2) { + rounds.push( + computeRoundFromPreviousRound( + roundIndex, + result.route_names[roundIndex] ? result.route_names[roundIndex]:'', + rounds[rounds.length - 1], + roundRank, + ), + ); + roundIndex++; + } + + // compute final and semi final + const semifinalRoundIndex = roundIndex++; + const semifinal = computeRoundFromPreviousRound( + semifinalRoundIndex, + result.route_names[semifinalRoundIndex] ? result.route_names[semifinalRoundIndex]:'', + rounds[rounds.length - 1], + 2, + true, + ); + computeWinnerOfPair(semifinal.pairs[0], semifinalRoundIndex); + rounds.push(semifinal); + + const finalRoundIndex = roundIndex; + const final = computeRoundFromPreviousRound( + finalRoundIndex, + result.route_names[finalRoundIndex] ? result.route_names[finalRoundIndex]:'', + rounds[rounds.length - 2], + 2, + ); + computeWinnerOfPair(final.pairs[0], finalRoundIndex); + rounds.push(final); + + return { + rounds: rounds, + }; +} diff --git a/resources/qml/Components/SpeedFlowChart.qml b/resources/qml/Components/SpeedFlowChart.qml index 9aabc6d..112efe9 100644 --- a/resources/qml/Components/SpeedFlowChart.qml +++ b/resources/qml/Components/SpeedFlowChart.qml @@ -22,6 +22,8 @@ import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.1 import QtGraphicalEffects 1.0 +import "SpeedFlowChart.js" as SpeedFlowChart + Item { id: control @@ -32,8 +34,6 @@ Item { property int refreshes: 0 property int roundRefreshes: 1 - property int roundCount: 0 - onFlowchartDataChanged: { prepareData() } @@ -42,181 +42,20 @@ Item { if(!control.enabled || control.flowchartData === undefined || control.flowchartData['route_names'] === undefined) return - /*refreshes += 1 - if(refreshes > 2){ - roundRefreshes += 1 - } - - console.log("refreshes: " + refreshes + " rounds: " + roundRefreshes) - - // create competition like data (just testing) - for(var part in flowchartData['participants']){ - if(flowchartData['participants'].hasOwnProperty(part)){ - - for(var r = 2 + roundRefreshes; r < 7; r++){ - delete flowchartData['participants'][part]["result"+r] - delete flowchartData['participants'][part]["result_rank"+r] - } - - if(parseInt(flowchartData['participants'][part]["result_rank0"]) > 14 + refreshes) { - delete flowchartData['participants'][part]["result_rank2"] - } - } - } - - - delete flowchartData['route_names'][2] - delete flowchartData['route_names'][3] - delete flowchartData['route_names'][4] - delete flowchartData['route_names'][5] - delete flowchartData['route_names'][6] - */ - - //flowchartData['route_names'] = flowchartData['route_names'].slice(0,) - - // array to store the restructured data - var allData = [] - control.allFlowchartData = [] - - control.rounds = Object.keys(control.flowchartData['route_names']).length > 2 ? control.flowchartData['route_names']["2"].includes("8") ? 2:1 : 0 - - //console.log(JSON.stringify(flowchartData['route_names'])) - - for(var round in flowchartData['route_names']){ - if(flowchartData['route_names'].hasOwnProperty(round) && parseInt(round) >= 0){ - //console.log(round) - - if(parseInt(round) === 0){ - // this is the first round - - // find pairs (always wors vs. best (1-16; 1-15; ...)) (they are sorted by the rank of the better athlete (1-2-3-4-5-6-7-8) - - var qualificationResults = [] - - for(var x = 0; x < flowchartData['participants'].length; x++){ - qualificationResults.push(flowchartData['participants'][x]) - } - - qualificationResults.sort(function(a, b) { - return parseInt(a["result_rank0"]) - parseInt(b["result_rank0"]); - }); - - var nextRoundPairs = [] - var totalMatches = (parseInt(Object.keys(control.flowchartData['route_names']).length > 2 ? control.flowchartData['route_names']["2"].includes("8") ? 2:1 : 0) + 2) - var nextRoundMatches = Math.pow(2, totalMatches-1) - - for(var i = 0; i < nextRoundMatches; i++){ - nextRoundPairs.push([qualificationResults[i], qualificationResults[nextRoundMatches*2-i-1]]) - } - - // build second round pairs (sorted by the rank of the better athlete and worst vs. best (1-8; 2-7; ... )) - - var sortedFirstRoundPairs = [] - - for(i = 0; i < nextRoundMatches; i += 1){ - sortedFirstRoundPairs.push([nextRoundPairs.shift(), nextRoundPairs.pop()]) - } - - // sort these pairs (containing two pairs of athletes) by the rank of the better athlete (1-4;2-3) - - var finalSortedFirstRoundPairs = [] - - for(i=0; i < nextRoundMatches/4; i++){ - finalSortedFirstRoundPairs.push(sortedFirstRoundPairs[i]) - finalSortedFirstRoundPairs.push(sortedFirstRoundPairs[nextRoundMatches/2-i-1]) - } - - // convert the list of pairs of pairs of athletes back to a single list of pairs of athletes - - var finalFirstRoundPairs = [] - - for(i=0; i < finalSortedFirstRoundPairs.length; i++){ - finalFirstRoundPairs.push(finalSortedFirstRoundPairs[i][0]) - finalFirstRoundPairs.push(finalSortedFirstRoundPairs[i][1]) - } - - // push the first round to all data - finalFirstRoundPairs.push(2) - finalFirstRoundPairs.push(flowchartData['route_names'][2]) - allData.push(finalFirstRoundPairs) - - } - else if(parseInt(round) > 0 ){ - // this is not the first round - - var nextRound = [] - - // only used when the current round is the 1/2 final - var smallFinal = [] - var Final = [] - - for(var i = 0; i < allData[allData.length-1].length-2; i+=1){ - - var thisPair = allData[allData.length-1][i] - var thisWinner - var thisLooser - var thisWinnerIsFirstOfNewPair = i%2 === 0 - - if(thisPair[0] === undefined || thisPair[1] === undefined){ - continue - } - - if(thisWinnerIsFirstOfNewPair){ - nextRound.push([]) - } - - //thisPair[0]["result_rank"] = thisPair[0]["result_rank"+round] - //thisPair[1]["result_rank"] = thisPair[1]["result_rank"+round] - - if(parseInt(thisPair[0]["result_rank"+round]) < parseInt(thisPair[1]["result_rank"+round])){ - thisWinner = thisPair[0] - thisLooser = thisPair[1] - } - else if(parseInt(thisPair[0]["result_rank"+round]) > parseInt(thisPair[1]["result_rank"+round])) { - thisWinner = thisPair[1] - thisLooser = thisPair[0] - } - else { - // no result yet!! - //console.log("got no winner yet, rank 0: " + thisPair[0]["result_rank"+round] + " rank 1: " + thisPair[1]["result_rank"+round]) - continue - } - - //console.log(thisWinner['firstname']+" has won in round " + round) - - if(round - control.rounds === 2){ - // if we are in the 1/2 final - - Final.push(thisWinner) - smallFinal.push(thisLooser) - } - else { - nextRound[nextRound.length-1].push(thisWinner) - } - } - - if(smallFinal.length > 0 && Final.length > 0){ - - // Final - allData.push([Final, parseInt(round)+2, flowchartData['route_names'][String(parseInt(round)+2)] + " / " + flowchartData['route_names'][String(parseInt(round)+1)] ]) - // small Final - allData.push([smallFinal, parseInt(round)+1]) - - //break - } - else { - nextRound.push(parseInt(round) + 1 ) - nextRound.push(flowchartData['route_names'][parseInt(round) + 1]) - allData.push(nextRound) - } - } - } - } - - control.allFlowchartData = allData - control.roundCount = (parseInt(Object.keys(control.flowchartData['route_names']).length > 2 ? control.flowchartData['route_names']["2"].includes("8") ? 2:1 : 0) + 2) - //console.log(JSON.stringify(allData)) + var flowchartResult = SpeedFlowChart.convertResultsToSpeedFlowchartResult(control.flowchartData) + const l = flowchartResult.rounds.length; + const dummy = { dummy: true }; + flowchartResult.rounds[l - 2].pairs = [ + dummy, + ...flowchartResult.rounds[l - 1].pairs, + ...flowchartResult.rounds[l - 2].pairs, + ]; + flowchartResult.rounds[l - 2].roundName = `${ + flowchartResult.rounds[l - 1].roundName + } / ${flowchartResult.rounds[l - 2].roundName}`; + flowchartResult.rounds.pop(); + control.allFlowchartData = flowchartResult } ListView { @@ -238,19 +77,18 @@ Item { orientation: ListView.LeftToRight boundsBehavior: ListView.StopAtBounds - model: control.roundCount + model: control.allFlowchartData.rounds.length delegate: Item { id: roundItem property int thisIndex: index - property int thisRound: thisRoundIsValid ? control.allFlowchartData[roundItem.thisIndex][control.allFlowchartData[roundItem.thisIndex].length-2]:-1 - property bool thisRoundIsValid: control.allFlowchartData !== undefined - && control.allFlowchartData[roundItem.thisIndex] !== undefined - && control.allFlowchartData[roundItem.thisIndex].length > 2 + property var thisData: control.allFlowchartData.rounds[thisIndex] - property bool thisIsLastRound: thisIndex === control.roundCount - 1 - property bool thisIsSemiFinal: thisIndex === control.roundCount - 2 && rectRep.model === 2 + property bool thisRoundIsValid: true + + property bool thisIsLastRound: thisIndex === roundListView.model - 1 + property bool thisIsSemiFinal: thisIndex === roundListView.model - 2 property int tileSize: (roundItem.height / 8 - roundNameLa.height) * 1.45 @@ -272,50 +110,26 @@ Item { font.pixelSize: height * 0.6 minimumPixelSize: 1 font.bold: true - text: roundItem.thisRoundIsValid - && control.allFlowchartData[roundItem.thisIndex][control.allFlowchartData[roundItem.thisIndex].length-1] !== undefined ? - control.allFlowchartData[roundItem.thisIndex][control.allFlowchartData[roundItem.thisIndex].length-1] : + text: roundItem.thisData.roundName ? + roundItem.thisData.roundName : "-" } Repeater { id: rectRep - model: Math.max( Math.pow(2, control.roundCount-1) * Math.pow(0.5, (roundItem.thisIndex)), 2) + model: roundItem.thisData.pairs.length + delegate: Item { id: matchItm property bool lowerPart: (index%2 > 0) - - property var matchData: roundItem.thisRoundIsValid ? - control.allFlowchartData[ - thisIsSmallFinal ? - roundItem.thisIndex+1 : - roundItem.thisIndex - ][ thisIsSmallFinal ? 0:matchItm.thisIndex]: - undefined - + property var thisData: roundItem.thisData.pairs[index] property int thisIndex: index - property int thisRound: parseInt(roundItem.thisRound) - (thisIsSmallFinal ? 1:0) - property var thisMatchData: thisMatchDataIsValid ? matchData:[] - - property bool thisMatchDataIsValid: (matchData !== undefined && matchData !== null && typeof matchData === "object" && matchData.length > 0) - property bool thisMatchIsOver: thisMatchDataIsValid && thisMatchData[0]['result_rank'+thisRound] !== undefined && thisMatchData[1]['result_rank'+thisRound] !== undefined - - property bool thisIsFinal: roundItem.thisIsLastRound && thisIndex === rectRep.model - 2 - property bool thisIsSmallFinal: roundItem.thisIsLastRound && thisIndex === rectRep.model - 1 - - property int winnerIndex: thisMatchIsOver ? (parseInt(thisMatchData[0]['result_rank'+thisRound]) < parseInt(thisMatchData[1]['result_rank'+thisRound]) ? 0:1) : -1 - - height: !roundItem.thisIsLastRound ? - (roundItem.height - roundNameLa.height) / rectRep.model - roundCol.spacing : - (thisIsFinal ? - (roundItem.height - roundNameLa.height) * 0.5 + roundItem.tileSize * 0.5 : - (roundItem.height - roundNameLa.height) - (roundItem.height - roundNameLa.height) * 0.5 + roundItem.tileSize * 0.5 - ) + height: (roundItem.height - roundNameLa.height) / rectRep.model - roundCol.spacing width: roundItem.width - onMatchDataChanged: { + onThisDataChanged: { fadeInPa.start() } @@ -365,6 +179,7 @@ Item { RectangularGlow { id: effect + visible: matchRect.visible anchors.fill: matchRect glowRadius: 0 spread: 0 @@ -381,10 +196,11 @@ Item { bottom: matchItm.thisIsFinal ? parent.bottom:undefined } - //anchors.verticalCenterOffset: matchItm.lowerPart ? 10:10 + visible: !matchItm.thisData.dummy + width: parent.width height: roundItem.tileSize - //scale: 0.9 + color: Material.dialogColor radius: height * 0.2 @@ -399,6 +215,13 @@ Item { model: 2 delegate: RowLayout { + id: laneRow + + property var thisData: index === 0 ? matchItm.thisData.laneA : matchItm.thisData.laneB + property var participant: thisData ? thisData.participant : undefined + property var result: thisData ? thisData.result : undefined + property var lane: index === 0 ? "A":"B" + property bool isWinner: matchItm.thisData.winner === laneRow.lane height: parent.height / 2 - parent.spacing width: parent.width @@ -417,11 +240,11 @@ Item { font.bold: true opacity: 0.7 - text: matchItm.thisMatchData[index] !== undefined ? + text: laneRow.participant ? ( - parseInt(matchItm.thisMatchData[index]['result_rank0']) < 10 ? - matchItm.thisMatchData[index]['result_rank0'] + " ": - matchItm.thisMatchData[index]['result_rank0'] + laneRow.participant.results[0].rank < 10 ? + laneRow.participant.results[0].rank + " ": + laneRow.participant.results[0].rank ): "" } @@ -436,21 +259,22 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft font.pixelSize: height * 0.5 - font.bold: matchItm.winnerIndex === index + font.bold: laneRow.isWinner elide: "ElideRight" - color: matchItm.winnerIndex === index ? Material.color(Material.Green):Material.primaryTextColor + color: laneRow.isWinner ? Material.color(Material.Green):Material.primaryTextColor - text: matchItm.thisMatchData[index] !== undefined ? matchItm.thisMatchData[index]['firstname'] + " " + matchItm.thisMatchData[index]['lastname'] :"-" + text: laneRow.participant ? laneRow.participant.firstName + " " + laneRow.participant.lastName :"-" } Rectangle { Layout.preferredHeight: parent.height * 0.8 Layout.preferredWidth: parent.width * 0.13 + visible: laneRow.participant && laneRow.participant.startNumber ? true:false radius: height / 2 - border.width: 1 + border.width: parent.height * 0.03 border.color: Material.frameColor color: "transparent" @@ -467,7 +291,7 @@ Item { horizontalAlignment: Text.AlignHCenter - text: matchItm.thisMatchData[index] !== undefined ? matchItm.thisMatchData[index]["start_number"]:"" + text: laneRow.participant ? laneRow.participant.startNumber:"" } } @@ -485,15 +309,11 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignRight font.pixelSize: height * 0.5 - font.bold: matchItm.winnerIndex === index + font.bold: laneRow.isWinner - color: matchItm.winnerIndex === index ? Material.color(Material.Green):Material.primaryTextColor + color: laneRow.isWinner ? Material.color(Material.Green):Material.primaryTextColor - text: matchItm.thisMatchData[index] !== undefined && matchItm.thisMatchData[index]['result'+matchItm.thisRound] !== undefined ? - ( parseFloat(matchItm.thisMatchData[index]['result'+matchItm.thisRound]) ? - (parseFloat(matchItm.thisMatchData[index]['result'+matchItm.thisRound]).toFixed(2)) - : matchItm.thisMatchData[index]['result'+matchItm.thisRound] ) - : "-" + text: laneRow.result ? laneRow.result.result : "" } } } @@ -516,7 +336,7 @@ Item { id: blueRockBadgeComponent BlueRockBadge { - width: roundItem.width + width: roundItem.width * 0.8 height: width * 0.25 } } diff --git a/resources/qml/main.qml b/resources/qml/main.qml index 8e33191..247913e 100644 --- a/resources/qml/main.qml +++ b/resources/qml/main.qml @@ -190,18 +190,6 @@ Window { Material.theme: appSettings.read("darkTheme") === "true" ? Material.Dark:Material.Light - Component.onCompleted: { - //loadingDl.open() - //app.openAthlete() // dorian: 53139 , rustam: 6933 , helen: 53300 - //openWidget({nation:'GER'}) - //mainStack.push("Pages/AthleteSearchPage.qml") - //openWidget({comp: 11651, cat: 26}) - //openWidget({person: 6623}) - //console.log(JSON.stringify(serverConn.getParamsFromUrl(""))) - //openWidgetFromUrl("https://l.bluerock.dev/?comp=11501&cat=GER_M") - //openWidgetFromUrl("https://www.digitalrock.de/egroupware/ranking/json.php?cat=12&comp=12171&type=") - } - FontLoader { id: fa5solid source: "qrc:/fonts/fa5solid.otf" diff --git a/resources/qml/qml.qrc b/resources/qml/qml.qrc index 323af13..be351c6 100644 --- a/resources/qml/qml.qrc +++ b/resources/qml/qml.qrc @@ -31,5 +31,6 @@ Components/SharePopup.qml Pages/QrCodeScanPage.qml Components/MovingLabel.qml + Components/SpeedFlowChart.js