import { useEffect, useMemo, useState } from 'react' import type { EnvironmentProfile } from '../platforms/types' import type { RuntimeImageArtifact } from './types' import type { VisualizationStack } from './WorkspaceVisualizationPanel' export type Nav2VisualizationMode = 'slam' | 'navigation' const DEFAULT_NAV2_VISUALIZATION_PROFILE: EnvironmentProfile = { id: 'default-nav2-visualization', week: 0, platformKey: 'futuredrive-local-pc', profileType: 'NAV2', name: 'NAV2', config: { type: 'NAV2', target: 'local', viewer: { stack: 'nav2' }, bridge: { name: 'nav2-ros-bridge', port: 8765, image: 'slam-ros-bridge:latest', buildContext: 'resources/slam-ros-bridge', env: { ROBOT_STACK: 'nav2' } } } } function resolveVisualizationStack(profileType: string): VisualizationStack | null { const type = profileType.toLowerCase() if (type === 'real_slam') return 'slam' if (type === 'real_navigation') return 'nav2' return type === 'autoware' || type === 'nav2' || type === 'slam' ? type : null } function resolveNav2Mode(profile: EnvironmentProfile | null): Nav2VisualizationMode { return profile?.profileType.toLowerCase() === 'real_navigation' ? 'navigation' : 'slam' } function isRealPlatformProfile(profile: EnvironmentProfile | null): boolean { const type = profile?.profileType.toLowerCase() return type === 'real_slam' || type === 'real_navigation' } function selectRuntimeProfiles(profiles: EnvironmentProfile[], remoteWorkspace: boolean): EnvironmentProfile[] { const matchingProfiles = profiles.filter((profile) => ( remoteWorkspace ? isRealPlatformProfile(profile) : !isRealPlatformProfile(profile) )) return matchingProfiles.length ? matchingProfiles : profiles } type NoticeTone = 'info' | 'warning' | 'error' type UseWorkspaceVisualizationOptions = { profiles: EnvironmentProfile[] clonePathPreview: string rosDomainId: string remoteWorkspace: boolean mapKey?: string | null worldLayoutVersion?: number onTitlebarNotice: (title: string, tone?: NoticeTone, addToList?: boolean) => void } type UseWorkspaceVisualizationResult = { visualizationStacks: VisualizationStack[] visualizationStack: VisualizationStack setVisualizationStack: (stack: VisualizationStack) => void visualizationRunning: boolean visualizationBusy: boolean visualizationViewerUrl: string nav2VisualizationMode: Nav2VisualizationMode setNav2VisualizationMode: (mode: Nav2VisualizationMode) => void activeEnvironmentProfile: EnvironmentProfile | null activeRuntimeImages: RuntimeImageArtifact[] refreshVisualizationStatus: () => Promise startVisualizationBridge: () => Promise stopVisualizationBridge: () => Promise } function formatVisualizationStackLabel(stack: VisualizationStack | null | undefined): string { if (stack === 'nav2') return 'Nav2' if (stack === 'slam') return 'SLAM' return 'Autoware' } export function useWorkspaceVisualization({ profiles, clonePathPreview, rosDomainId, remoteWorkspace, mapKey, worldLayoutVersion, onTitlebarNotice }: UseWorkspaceVisualizationOptions): UseWorkspaceVisualizationResult { const [visualizationStack, setVisualizationStack] = useState('autoware') const [nav2VisualizationMode, setNav2VisualizationMode] = useState('slam') const [visualizationRunning, setVisualizationRunning] = useState(false) const [, setVisualizationStatusMessage] = useState('대기') const [visualizationBusy, setVisualizationBusy] = useState(false) const runtimeProfiles = useMemo(() => { const candidates = selectRuntimeProfiles(profiles, remoteWorkspace) const nav2Profiles = candidates.filter((profile) => resolveVisualizationStack(profile.profileType) !== 'autoware') return nav2Profiles.length ? nav2Profiles : [DEFAULT_NAV2_VISUALIZATION_PROFILE] }, [profiles, remoteWorkspace]) const visualizationStacks = useMemo(() => ( runtimeProfiles .map((profile) => resolveVisualizationStack(profile.profileType)) .filter((stack): stack is VisualizationStack => stack !== null) ), [runtimeProfiles]) const activeEnvironmentProfile = runtimeProfiles.find((profile) => resolveVisualizationStack(profile.profileType) === visualizationStack) ?? runtimeProfiles[0] ?? null const activeEnvironmentConfig = activeEnvironmentProfile?.config ?? {} const activeRuntimeImages = Array.isArray(activeEnvironmentConfig.runtimeImages) ? activeEnvironmentConfig.runtimeImages.filter((image): image is RuntimeImageArtifact => ( typeof image === 'object' && image !== null && typeof (image as RuntimeImageArtifact).image === 'string' )) : [] const activeEnvironmentAgent = typeof activeEnvironmentConfig.agent === 'object' && activeEnvironmentConfig.agent !== null ? activeEnvironmentConfig.agent as Record : null const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string' ? activeEnvironmentAgent.url.replace(/\/+$/, '') : '' const visualizationRemote = activeEnvironmentConfig.target === 'remote-jetson' && activeEnvironmentAgentUrl const visualizationRemoteBridgeHttpUrl = (() => { if (!visualizationRemote) return '' try { const url = new URL(activeEnvironmentAgentUrl) url.port = '8765' return url.toString().replace(/\/+$/, '') } catch { return '' } })() const visualizationBridgeHttpUrl = visualizationRemoteBridgeHttpUrl || 'http://localhost:8765' const visualizationBridgeUrl = visualizationRemote ? visualizationBridgeHttpUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:') : 'ws://localhost:8765' const visualizationBackendUrl = visualizationBridgeHttpUrl const visualizationViewerUrl = visualizationStack === 'slam' || visualizationStack === 'nav2' ? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRealPlatformProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}` : `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}` useEffect(() => { const stack = runtimeProfiles[0] ? resolveVisualizationStack(runtimeProfiles[0].profileType) : null if (stack) setVisualizationStack(stack) setNav2VisualizationMode(resolveNav2Mode(runtimeProfiles[0] ?? null)) }, [runtimeProfiles]) const refreshVisualizationStatus = async (): Promise => { const status = await window.localWorkspace.visualization.status().catch((error) => ({ ok: false, running: false, stack: null, message: error instanceof Error ? error.message : String(error) })) setVisualizationRunning(Boolean(status.running)) if (status.stack) setVisualizationStack(status.stack) setVisualizationStatusMessage(status.running ? formatVisualizationStackLabel(status.stack) : status.message ? status.message : '대기') } const startVisualizationBridge = async (): Promise => { if (visualizationBusy) return setVisualizationBusy(true) setVisualizationStatusMessage('시작 중') try { const status = await window.localWorkspace.visualization.start(visualizationStack, clonePathPreview, rosDomainId, activeEnvironmentProfile.config) if (!status.ok) throw new Error(status.message ?? '시작 실패') setVisualizationRunning(Boolean(status.running)) if (status.stack) setVisualizationStack(status.stack) setVisualizationStatusMessage(formatVisualizationStackLabel(status.stack)) onTitlebarNotice('화면이 시작되었습니다', 'info') } catch (error) { setVisualizationRunning(false) setVisualizationStatusMessage(error instanceof Error ? error.message : '시작 실패') onTitlebarNotice('화면 시작에 실패했습니다', 'error') } finally { setVisualizationBusy(false) } } const stopVisualizationBridge = async (): Promise => { if (visualizationBusy) return setVisualizationBusy(true) setVisualizationStatusMessage('종료 중') try { const status = await window.localWorkspace.visualization.stop(activeEnvironmentProfile?.config) if (!status.ok) throw new Error(status.message ?? '종료 실패') setVisualizationRunning(false) setVisualizationStatusMessage('대기') onTitlebarNotice('화면이 종료되었습니다', 'info') } catch (error) { setVisualizationStatusMessage(error instanceof Error ? error.message : '종료 실패') onTitlebarNotice('화면 종료에 실패했습니다', 'error') } finally { setVisualizationBusy(false) } } return { visualizationStacks, visualizationStack, setVisualizationStack, visualizationRunning, visualizationBusy, visualizationViewerUrl, nav2VisualizationMode, setNav2VisualizationMode, activeEnvironmentProfile, activeRuntimeImages, refreshVisualizationStatus, startVisualizationBridge, stopVisualizationBridge } }