You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Application-Web/src/components/Visualizer.tsx

261 lines
7.4 KiB

import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import { APITacticService } from "../service/APITacticService.ts"
import {
CourtType,
StepContent,
StepInfoNode,
TacticComponent,
} from "../model/tactic/Tactic.ts"
import { getParent } from "../domains/StepsDomain.ts"
import {
computeRelativePositions,
getPhantomInfo,
} from "../domains/PlayerDomains.ts"
import { PlayerInfo, PlayerLike } from "../model/tactic/Player.ts"
import { CourtPlayer } from "./editor/CourtPlayer.tsx"
import { BALL_TYPE } from "../model/tactic/CourtObjects.ts"
import { CourtBallPiece } from "./editor/CourtBall.tsx"
import { CourtAction } from "./editor/CourtAction.tsx"
import { BasketCourt, Court } from "./editor/BasketCourt.tsx"
import { TacticService } from "../service/MutableTacticService.ts"
import { useAppFetcher } from "../App.tsx"
import { mapIdentifiers } from "../domains/TacticContentDomains.ts"
export interface VisualizerProps {
tacticId: number
stepId?: number
visualizerId: string | number
}
export function Visualizer({
visualizerId,
tacticId,
stepId,
}: VisualizerProps) {
const [panicMessage, setPanicMessage] = useState<string | null>(null)
const [courtType, setCourtType] = useState<CourtType | null>()
const [stepsTree, setStepsTree] = useState<StepInfoNode | null>()
const fetcher = useAppFetcher()
const service = useMemo(
() => new APITacticService(fetcher, tacticId),
[tacticId],
)
const isNotInit = !stepsTree || !courtType
useEffect(() => {
async function init() {
const contextResult = await service.getContext()
if (typeof contextResult === "string") {
setPanicMessage(contextResult)
return
}
const rootStep = contextResult.stepsTree
setStepsTree(rootStep)
setCourtType(contextResult.courtType)
}
if (isNotInit) init()
}, [isNotInit, service])
if (panicMessage) {
return <p>{panicMessage}</p>
}
if (isNotInit) {
return <p>Loading...</p>
}
return (
<StepVisualizer
visualizerId={visualizerId}
courtType={courtType}
stepsTree={stepsTree}
stepId={stepId}
service={service}
/>
)
}
export interface StepVisualizerProps {
stepId?: number
visualizerId: string | number
stepsTree: StepInfoNode
courtType: CourtType
service: TacticService
}
export function StepVisualizer({
stepId,
visualizerId,
stepsTree,
courtType,
service,
}: StepVisualizerProps) {
const [panicMessage, setPanicMessage] = useState<string | null>(null)
const [content, setContent] = useState<StepContent | null>(null)
const [parentContent, setParentContent] = useState<StepContent | null>(null)
const isNotInit = !content
useEffect(() => {
async function init() {
const contentStepId = stepId ?? stepsTree.id
const contentResult = await service.getContent(contentStepId)
if (typeof contentResult === "string") {
setPanicMessage(contentResult)
return
}
const stepParent = getParent(stepsTree, contentStepId)
let parentContent = null
if (stepParent) {
const parentResult = await service.getContent(contentStepId)
if (typeof parentResult === "string") {
setPanicMessage(parentResult)
return
}
parentContent = parentResult
}
setContent(
mapIdentifiers(contentResult, (id) => `${id}-${visualizerId}`),
)
if (parentContent) {
setParentContent(
mapIdentifiers(
parentContent,
(id) => `${id}-${visualizerId}-parent`,
),
)
}
}
if (isNotInit) init()
}, [isNotInit, visualizerId, service, stepId, stepsTree])
if (panicMessage) {
return <p>{panicMessage}</p>
}
if (isNotInit) {
return <p>Loading Content...</p>
}
return (
<VisualizerFrame
content={content}
parentContent={parentContent}
courtType={courtType}
/>
)
}
export interface VisualizerFrameProps {
content: StepContent
parentContent: StepContent | null
courtType: CourtType
}
export function VisualizerFrame({
content,
parentContent,
courtType,
}: VisualizerFrameProps) {
const courtRef = useRef<HTMLDivElement>(null)
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const relativePositions = useMemo(() => {
const courtBounds = courtRef.current?.getBoundingClientRect()
return courtBounds
? computeRelativePositions(courtBounds, content)
: new Map()
}, [content, courtRef])
const renderPlayer = useCallback(
(component: PlayerLike, isFromParent: boolean) => {
let info: PlayerInfo
const isPhantom = component.type == "phantom"
const usedContent = isFromParent ? parentContent! : content
if (isPhantom) {
info = getPhantomInfo(
component,
usedContent,
relativePositions,
courtBounds(),
)
} else {
info = component
}
const className =
(isPhantom ? "phantom" : "player") +
" " +
(isFromParent ? "from-parent" : "")
return (
<CourtPlayer
key={component.id}
playerInfo={info}
className={className}
availableActions={() => []}
/>
)
},
[content, courtBounds, parentContent, relativePositions],
)
const renderComponent = useCallback(
(component: TacticComponent, isFromParent: boolean): ReactNode => {
if (component.type === "player" || component.type === "phantom") {
return renderPlayer(component, isFromParent)
}
if (component.type === BALL_TYPE) {
return <CourtBallPiece key="ball" pos={component.pos} />
}
return <></>
},
[renderPlayer],
)
const renderActions = useCallback(
(component: TacticComponent, isFromParent: boolean) =>
component.actions.map((action, i) => {
return (
<CourtAction
key={"action-" + component.id + "-" + i}
action={action}
origin={component.id}
courtRef={courtRef}
color={isFromParent ? "gray" : "black"}
isEditable={false}
/>
)
}),
[courtRef],
)
return (
<BasketCourt
parentComponents={parentContent?.components ?? null}
components={content.components}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
renderComponent={renderComponent}
renderActions={renderActions}
/>
)
}