diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java index 092b950..2f0e556 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java @@ -7,6 +7,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.ArrayList; +import java.util.List; + @Getter @Setter @AllArgsConstructor @@ -36,4 +39,20 @@ public class TreeModel { @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinColumn(referencedColumnName = "id") TreeModel right; + + public List flat() { + List out = new ArrayList<>(); + this.flat(out); + return out; + } + + private void flat(List out) { + out.add(this); + + if (this.right != null) + this.right.flat(out); + + if (this.left != null) + this.left.flat(out); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index 85e8872..c184f3d 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -1,18 +1,17 @@ package fr.titionfire.ffsaf.ws.recv; -import fr.titionfire.ffsaf.data.model.CategoryModel; -import fr.titionfire.ffsaf.data.model.MatchModel; -import fr.titionfire.ffsaf.data.repository.CategoryRepository; -import fr.titionfire.ffsaf.data.repository.CombRepository; -import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.data.model.*; +import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.domain.entity.MatchEntity; +import fr.titionfire.ffsaf.domain.entity.TreeEntity; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; import fr.titionfire.ffsaf.ws.PermLevel; import fr.titionfire.ffsaf.ws.send.SSMatch; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.panache.common.Sort; import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.websockets.next.WebSocketConnection; import io.smallrye.mutiny.Uni; @@ -21,9 +20,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.jboss.logging.Logger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.stream.Stream; @WithSession @@ -41,6 +38,9 @@ public class RMatch { @Inject CategoryRepository categoryRepository; + @Inject + TreeRepository treeRepository; + @Inject CompetitionGuestRepository competitionGuestRepository; @@ -140,6 +140,140 @@ public class RMatch { .replaceWithVoid(); } + @WSReceiver(code = "updateMatchScore", permission = PermLevel.TABLE) + public Uni updateMatchScore(WebSocketConnection connection, MatchScore score) { + return getById(score.matchId(), connection) + .chain(matchModel -> { + int old_win = matchModel.win(); + Optional optional = matchModel.getScores().stream() + .filter(s -> s.getN_round() == score.n_round()).findAny(); + boolean b = score.s1() != -1000 || score.s2() != -1000; + if (optional.isPresent()) { + if (b) { + optional.get().setS1(score.s1()); + optional.get().setS2(score.s2()); + } else { + matchModel.getScores().remove(optional.get()); + } + } else if (b) { + matchModel.getScores().add(new ScoreEmbeddable(score.n_round(), score.s1(), score.s2())); + } + + return Panache.withTransaction(() -> matchRepository.persist(matchModel)) + .call(mm -> { + if (mm.isEnd() && mm.win() != old_win && mm.getCategory_ord() == -42) { + return updateEndAndTree(mm, new ArrayList<>()) + .call(l -> SSMatch.sendMatch(connection, l)); + } + return Uni.createFrom().nullItem(); + }); + }) + .call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm))) + .replaceWithVoid(); + } + + @WSReceiver(code = "updateMatchEnd", permission = PermLevel.TABLE) + public Uni updateMatchEnd(WebSocketConnection connection, MatchEnd matchEnd) { + List toSend = new ArrayList<>(); + + return getById(matchEnd.matchId(), connection) + .chain(mm -> { + if (mm.getCategory_ord() == -42 && mm.win() == 0) { // Tournois + mm.setDate(null); + mm.setEnd(false); + } else { + mm.setDate((matchEnd.end) ? new Date() : null); + mm.setEnd(matchEnd.end); + } + return Panache.withTransaction(() -> matchRepository.persist(mm)); + }) + .invoke(mm -> toSend.add(MatchEntity.fromModel(mm))) + .chain(mm -> updateEndAndTree(mm, toSend)) + .call(__ -> SSMatch.sendMatch(connection, toSend)) + .replaceWithVoid(); + } + + private Uni> updateEndAndTree(MatchModel mm, List toSend) { + return (mm.getCategory_ord() != -42) ? + Uni.createFrom().item(toSend) : + treeRepository.list("category = ?1 AND level != 0", Sort.ascending("level"), mm.getCategory().getId()) + .chain(treeModels -> { + List node = treeModels.stream().flatMap(t -> t.flat().stream()).toList(); + List trees = treeModels.stream().map(TreeEntity::fromModel).toList(); + for (int i = 0; i < trees.size() - 1; i++) { + TreeEntity.setAssociated(trees.get(i), trees.get(i + 1)); + } + + TreeEntity root = trees.stream() + .filter(t -> t.getMatchNode(mm.getId()) != null) + .findFirst() + .orElseThrow(); + + TreeEntity currentNode = root.getMatchNode(mm.getId()); + if (currentNode == null) { + LOGGER.error( + "currentNode is empty for " + mm.getId() + " in " + mm.getCategory().getId()); + return Uni.createFrom().voidItem(); + } + + TreeEntity parent = TreeEntity.getParent(root, currentNode); + if (parent == null) { + LOGGER.error("parent is empty for " + mm.getId() + " in " + mm.getCategory().getId()); + return Uni.createFrom().voidItem(); + } + + int w = mm.win(); + MembreModel toSetWin = null; + MembreModel toSetLose = null; + CompetitionGuestModel toSetWinGuest = null; + CompetitionGuestModel toSetLoseGuest = null; + + if (mm.isEnd() && w != 0) { + toSetWin = (w > 0) ? mm.getC1_id() : mm.getC2_id(); + toSetLose = (w > 0) ? mm.getC2_id() : mm.getC1_id(); + toSetWinGuest = (w > 0) ? mm.getC1_guest() : mm.getC2_guest(); + toSetLoseGuest = (w > 0) ? mm.getC2_guest() : mm.getC1_guest(); + } + + MatchModel modelWin = node.stream() + .filter(n -> Objects.equals(n.getId(), parent.getId())) + .findAny() + .map(TreeModel::getMatch) + .orElseThrow(); + + MatchModel modelLose = (parent.getAssociatedNode() == null) ? null : node.stream() + .filter(n -> Objects.equals(n.getId(), parent.getAssociatedNode().getId())) + .findAny() + .map(TreeModel::getMatch) + .orElseThrow(); + + if (currentNode.equals(parent.getLeft())) { + modelWin.setC1_id(toSetWin); + modelWin.setC1_guest(toSetWinGuest); + if (modelLose != null) { + modelLose.setC1_id(toSetLose); + modelLose.setC1_guest(toSetLoseGuest); + } + } else if (currentNode.equals(parent.getRight())) { + modelWin.setC2_id(toSetWin); + modelWin.setC2_guest(toSetWinGuest); + if (modelLose != null) { + modelLose.setC2_id(toSetLose); + modelLose.setC2_guest(toSetLoseGuest); + } + } + + return Panache.withTransaction(() -> matchRepository.persist(modelWin) + .invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2))) + .call(__ -> modelLose == null ? Uni.createFrom().nullItem() : + matchRepository.persist(modelLose) + .invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2))) + ) + .replaceWithVoid()); + }) + .map(__ -> toSend); + } + @WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN) public Uni deleteMatch(WebSocketConnection connection, Long idMatch) { return getById(idMatch, connection) @@ -190,6 +324,14 @@ public class RMatch { public record MatchComb(long id, Long c1, Long c2) { } + @RegisterForReflection + public record MatchScore(long matchId, int n_round, int s1, int s2) { + } + + @RegisterForReflection + public record MatchEnd(long matchId, boolean end) { + } + @RegisterForReflection public record MatchOrder(long id, long pos) { } diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx new file mode 100644 index 0000000..f487c09 --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -0,0 +1,476 @@ +import React, {useEffect, useReducer, useRef, useState} from "react"; +import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; +import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; +import {MarchReducer} from "../../../utils/MatchReducer.jsx"; +import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; +import {DrawGraph} from "../../result/DrawGraph.jsx"; +import {scorePrint, win} from "../../../utils/Tools.js"; + +function CupImg() { + return +} + +export function CMTable() { + const [catId, setCatId] = useState(-1); + + return
+
+
+
+ A +
+
+ B +
+
+
+
+
Matches
+
+ +
+
+
+ D +
+
+
+
+} + +function CategorieSelect({catId, setCatId}) { + const setLoading = useLoadingSwitcher() + const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); + const {dispatch} = useWS(); + + useEffect(() => { + const categoryListener = ({data}) => { + setCats([...cats.filter(c => c.id !== data.id), data]) + } + dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) + return () => dispatch({type: 'removeListener', payload: categoryListener}) + }, [cats]); + + const cat = cats?.find(c => c.id === catId); + + return <> +
+
Catégorie
+ +
+ {catId !== -1 && } + +} + +function MatchPanel({catId, cat}) { + const setLoading = useLoadingSwitcher() + const {sendRequest, dispatch} = useWS(); + const [trees, setTrees] = useState([]); + const [matches, reducer] = useReducer(MarchReducer, []); + const combDispatch = useCombsDispatch(); + + function readAndConvertMatch(matches, data, combsToAdd) { + matches.push({...data, c1: data.c1?.id, c2: data.c2?.id}) + if (data.c1) + combsToAdd.push(data.c1) + if (data.c2) + combsToAdd.push(data.c2) + } + + useEffect(() => { + if (!catId) + return; + setLoading(1); + sendRequest('getFullCategory', catId) + .then((data) => { + setTrees(data.trees.map(d => from_sendTree(d, true))) + + let matches2 = []; + let combsToAdd = []; + data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); + data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); + + reducer({type: 'REPLACE_ALL', payload: matches2}); + combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}}); + }).finally(() => setLoading(0)) + + const treeListener = ({data}) => { + if (data.length < 1 || data[0].categorie !== catId) + return + setTrees(data.map(d => from_sendTree(d, true))) + + let matches2 = []; + let combsToAdd = []; + data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); + reducer({type: 'REPLACE_TREE', payload: matches2}); + combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}}); + } + + const matchListener = ({data: datas}) => { + for (const data of datas) { + reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}}) + combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}}) + } + } + + const matchOrder = ({data}) => { + reducer({type: 'REORDER', payload: data}) + } + + const deleteMatch = ({data: datas}) => { + for (const data of datas) + reducer({type: 'REMOVE', payload: data}) + } + + dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}}) + dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}}) + dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}}) + dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}}) + return () => { + dispatch({type: 'removeListener', payload: treeListener}) + dispatch({type: 'removeListener', payload: matchListener}) + dispatch({type: 'removeListener', payload: matchOrder}) + dispatch({type: 'removeListener', payload: deleteMatch}) + } + }, [catId]); + + return +} + +function ListMatch({cat, matches, trees}) { + const [type, setType] = useState(1); + + useEffect(() => { + if ((cat.type & type) === 0) + setType(cat.type); + }, [cat]); + + return
+ {cat.type === 3 && <> +
    +
  • +
    setType(1)}>Poule +
    +
  • +
  • +
    setType(2)}>Tournois +
    +
  • +
+ + } + + {type === 1 && <> + + } + + {type === 2 && <> + + } +
+} + +function MatchList({matches, cat}) { + const [activeMatch, setActiveMatch] = useState(null) + + const liceName = (cat.liceName || "N/A").split(";"); + const marches2 = matches.filter(m => m.categorie_ord !== -42) + .sort((a, b) => a.categorie_ord - b.categorie_ord) + .map(m => ({...m, win: win(m.scores)})) + + //useEffect(() => { + // if (activeMatch !== null) + // setActiveMatch(null); + //}, [cat]) + + useEffect(() => { + if (marches2.length === 0) + return; + if (marches2.some(m => m.id === activeMatch)) + return; + + setActiveMatch(marches2.find(m => !m.end)?.id); + }, [matches]) + + const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; + return <> +
+ + + + + + + + + + + + + + {marches2.map((m, index) => ( + setActiveMatch(m.id)}> + + + + + + + + + ))} + +
LPRougeBlue
+ {liceName[(index - firstIndex) % liceName.length]}{m.poule} + {index >= firstIndex ? index + 1 - firstIndex : ""}{m.end && m.win > 0 && } + + {m.end && m.win < 0 && }
+
+ + {activeMatch && } + +} + +function BuildTree({treeData, matches}) { + const scrollRef = useRef(null) + const [currentMatch, setCurrentMatch] = useState(null) + const {getComb} = useCombs() + + function parseTree(data_in) { + if (data_in?.data == null) + return null + + const matchData = matches.find(m => m.id === data_in.data) + const c1 = getComb(matchData?.c1) + const c2 = getComb(matchData?.c2) + + + let node = new TreeNode({ + ...matchData, + c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, + c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null + }) + node.left = parseTree(data_in?.left) + node.right = parseTree(data_in?.right) + + return node + } + + function initTree(data_in) { + let out = [] + for (const din of data_in) { + out.push(parseTree(din)) + } + return out + } + + const trees = initTree(treeData); + + const onMatchClick = (rect, matchId, __) => { + setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees)}); + } + + const onClickVoid = () => { + } + + + return
+
+ +
+ + {currentMatch?.matchSelect && } +
+} + + +function ScorePanel({matchId, matches}) { + const {sendRequest} = useWS() + const setLoading = useLoadingSwitcher() + + const match = matches.find(m => m.id === matchId) + + const [end, setEnd] = useState(match?.end || false) + const [scoreIn, setScoreIn] = useState("") + const inputRef = useRef(null) + const tableRef = useRef(null) + const scoreRef = useRef([]) + const lastScoreClick = useRef(null) + + const handleScoreClick = (e, round, comb) => { + e.stopPropagation(); + const tableRect = tableRef.current.getBoundingClientRect(); + const rect = e.currentTarget.getBoundingClientRect(); + + updateScore(); + + const sel = inputRef.current; + sel.style.top = (rect.y - tableRect.y) + "px"; + sel.style.left = (rect.x - tableRect.x) + "px"; + sel.style.width = rect.width + "px"; + sel.style.height = rect.height + "px"; + sel.style.display = "block"; + + if (round === -1) { + const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; + setScoreIn(""); + console.log("Setting for new round", maxRound); + lastScoreClick.current = {matchId: matchId, round: maxRound, comb}; + } else { + const score = match.scores.find(s => s.n_round === round); + setScoreIn((comb === 1 ? score?.s1 : score?.s2) || ""); + lastScoreClick.current = {matchId: matchId, round, comb}; + setTimeout(() => inputRef.current.select(), 100); + } + } + + const updateScore = () => { + if (lastScoreClick.current !== null) { + const {matchId, round, comb} = lastScoreClick.current; + lastScoreClick.current = null; + + const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn); + + const score = match.scores.find(s => s.n_round === round); + let newScore; + if (score) { + if (comb === 1) + newScore = {...score, s1: scoreIn_}; + else + newScore = {...score, s2: scoreIn_}; + + if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2) + return + } else { + newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)}; + if (newScore.s1 === -1000 && newScore.s2 === -1000) + return + } + + console.log("Updating score", matchId, newScore); + + setLoading(1) + sendRequest('updateMatchScore', {matchId: matchId, ...newScore}) + .finally(() => { + setLoading(0) + }) + } + } + + const onClickVoid = () => { + updateScore(); + + const sel = inputRef.current; + sel.style.display = "none"; + lastScoreClick.current = null; + } + + useEffect(() => { + if (!match || match?.end === end) + return; + + setLoading(1) + sendRequest('updateMatchEnd', {matchId: matchId, end}) + .finally(() => { + setLoading(0) + }) + }, [end]); + + useEffect(() => { + onClickVoid() + }, [matchId]); + + useEffect(() => { + if (match?.scores) + scoreRef.current = scoreRef.current.slice(0, match.scores.length); + }, [match?.scores]); + + useEffect(() => { + if (!match) + return; + setEnd(match.end); + }, [match]); + + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') + const o = [...tooltipTriggerList] + o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) + + const tt = "Score speciaux :
" + + "-997 : disqualifié
" + + "-998 : absent
" + + "-999 : forfait" + + const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; + return
+
+
Scores
+ + + + + + + + + + {match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => ( + + + + + + ))} + + + + + + +
MancheRougeBleu
{score.n_round + 1} scoreRef.current[score.n_round * 2] = e} + onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)} scoreRef.current[score.n_round * 2 + 1] = e} + onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}
scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>- scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>- +
+
+
+ setEnd(e.target.checked)}/> + +
+
+ setScoreIn(e.target.value)} + onClick={e => e.stopPropagation()} + onKeyDown={e => { + if (e.key === "Tab") { + if (lastScoreClick.current !== null) { + const {round, comb} = lastScoreClick.current; + const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1); + if (nextIndex >= 0 && nextIndex < scoreRef.current.length) { + e.preventDefault(); + scoreRef.current[nextIndex].click(); + } + } + } else if (e.key === "Enter") { + e.preventDefault(); + onClickVoid(); + } + }}/> +
+
+
+
+} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 9d1b713..fab5e33 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -6,7 +6,7 @@ import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; import {DrawGraph} from "../../result/DrawGraph.jsx"; import {SelectCombModalContent} from "./SelectCombModalContent.jsx"; -import {createMatch, scoreToString, win} from "../../../utils/CompetitionTools.js"; +import {createMatch, scoreToString} from "../../../utils/CompetitionTools.js"; import {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core'; import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable'; @@ -15,6 +15,7 @@ import {CSS} from '@dnd-kit/utilities'; import {toast} from "react-toastify"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faTrash} from "@fortawesome/free-solid-svg-icons"; +import {win} from "../../../utils/Tools.js"; function CupImg() { return }/> }/> - }/> + }/> @@ -80,31 +81,18 @@ function WSStatus({setPerm}) { function Home2({perm}) { const nav = useNavigate(); - const {sendRequest} = useWS(); return

Sélectionne les modes d'affichage

- {perm === "admin" && <> + {perm === "ADMIN" && <> } - {perm === "table" && <> + {perm === "TABLE" && <> }
- - -
} diff --git a/src/main/webapp/src/pages/result/DrawGraph.jsx b/src/main/webapp/src/pages/result/DrawGraph.jsx index 4224f9c..640fdf5 100644 --- a/src/main/webapp/src/pages/result/DrawGraph.jsx +++ b/src/main/webapp/src/pages/result/DrawGraph.jsx @@ -1,5 +1,5 @@ import {useEffect, useRef} from "react"; -import {win} from "../../utils/CompetitionTools.js"; +import {scorePrint, win} from "../../utils/Tools.js"; const max_x = 500; const size = 24; @@ -18,13 +18,17 @@ export function DrawGraph({ onMatchClick = function (rect, match, comb) { }, onClickVoid = function () { - } + }, + matchSelect = null, + matchNext = null, }) { const canvasRef = useRef(null); const actionCanvasRef = useRef(null); const ctxARef = useRef(null); const actionMapRef = useRef({}); + const selectColor = "#30cc30"; + function getBounds(root) { let px = max_x; let py; @@ -131,7 +135,7 @@ export function DrawGraph({ ctx.textBaseline = 'top'; for (let i = 0; i < scores.length; i++) { - const score = scores[i].s1 + "-" + scores[i].s2; + const score = scorePrint(scores[i].s1) + "-" + scorePrint(scores[i].s2); const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; const text = ctx.measureText(score); let dx = (size * 2 - text.width) / 2; @@ -192,6 +196,20 @@ export function DrawGraph({ ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height) actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2} + if (matchSelect && match.id === matchSelect) { + ctx.strokeStyle = selectColor + ctx.strokeRect(px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0), size * 8, size * 2 + (size * 1.5 | 0)) + ctx.strokeStyle = "#000000" + } + + if (matchNext && match.id === matchNext) { + ctx.strokeStyle = selectColor + ctx.setLineDash([15, 10]) + ctx.strokeRect(px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0), size * 8, size * 2 + (size * 1.5 | 0)) + ctx.setLineDash([]) + ctx.strokeStyle = "#000000" + } + if (max_y.current < py + size + ((size * 1.5 / 2) | 0)) { max_y.current = py + size + (size * 1.5 / 2 | 0); } @@ -223,6 +241,23 @@ export function DrawGraph({ ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height) actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2} + + if (matchSelect && match.id === matchSelect) { + ctx.strokeStyle = selectColor + ctx.strokeRect(px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0), size * 8, + py + size * 2 * death - (size * 1.5 / 2 | 0) - (py - size * 2 * death - (size * 1.5 / 2 | 0)) + (size * 1.5 | 0)) + ctx.strokeStyle = "#000000" + } + + if (matchNext && match.id === matchNext) { + ctx.strokeStyle = selectColor + ctx.setLineDash([15, 10]) + ctx.strokeRect(px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0), size * 8, + py + size * 2 * death - (size * 1.5 / 2 | 0) - (py - size * 2 * death - (size * 1.5 / 2 | 0)) + (size * 1.5 | 0)) + ctx.setLineDash([]) + ctx.strokeStyle = "#000000" + } + if (max_y.current < py + size * 2 * death + ((size * 1.5 / 2) | 0)) { max_y.current = py + size * 2 * death + ((size * 1.5 / 2 | 0)); } diff --git a/src/main/webapp/src/utils/CompetitionTools.js b/src/main/webapp/src/utils/CompetitionTools.js index 30c9f8a..81e14d2 100644 --- a/src/main/webapp/src/utils/CompetitionTools.js +++ b/src/main/webapp/src/utils/CompetitionTools.js @@ -1,28 +1,6 @@ -export function win(scores) { - let sum = 0 - for (const score of scores) { - if (score.s1 === -1000 || score.s2 === -1000) continue - if (score.s1 > score.s2) sum++ - else if (score.s1 < score.s2) sum-- - } - return sum -} +import {scorePrint} from "./Tools.js"; export function scoreToString(score) { - const scorePrint = (s1) => { - switch (s1) { - case -997: - return "disc." - case -998: - return "abs." - case -999: - return "for." - case -1000: - return "" - default: - return String(s1) - } - } if (score === null || score === undefined || score.length === 0) return "" diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 4215fcd..21e9232 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -105,3 +105,28 @@ export function getCatName(cat) { return cat; } } + +export function win(scores) { + let sum = 0 + for (const score of scores) { + if (score.s1 === -1000 || score.s2 === -1000) continue + if (score.s1 > score.s2) sum++ + else if (score.s1 < score.s2) sum-- + } + return sum +} + +export function scorePrint(s1) { + switch (s1) { + case -997: + return "disc." + case -998: + return "abs." + case -999: + return "for." + case -1000: + return "" + default: + return String(s1) + } +} diff --git a/src/main/webapp/src/utils/TreeUtils.js b/src/main/webapp/src/utils/TreeUtils.js index d62c59c..14b82b1 100644 --- a/src/main/webapp/src/utils/TreeUtils.js +++ b/src/main/webapp/src/utils/TreeUtils.js @@ -48,6 +48,25 @@ TreeNode.prototype = { this.left.__flat(out) if (this.right != null) this.right.__flat(out) + }, + nextMatchTree: function (trees) { + const tmpData = {keep_death: -1, keep_data: null} + for (const treeNode of trees) + this.__nextMatchTree(treeNode, 0, tmpData) + return tmpData.keep_data; + }, + __nextMatchTree: function (treeNode, death, tmpData) { + if (treeNode == null) + return; + + const match = treeNode.data + if (!match.end && death > tmpData.keep_death && this.data !== match.id) { + tmpData.keep_death = death; + tmpData.keep_data = match.id; + } + + this.__nextMatchTree(treeNode.left, death + 1, tmpData); + this.__nextMatchTree(treeNode.right, death + 1, tmpData); } }