import type { GuideRouteReadiness, GuideRouteResult, GuideRouteTarget } from '@/domain/museum' import { NAV_ROUTE_READINESS } from '@/domain/guideReadiness' import { isIndoorNavigableFloor } from '@/domain/guideFloor' import { GuideRouteError } from '@/repositories/GuideRouteRepository' import { defaultSgsSdkApiProvider, type SgsFloorDiagnosticsPayload, type SgsNavigablePlacePayload, type SgsSdkApiProvider } from '@/data/providers/sgsSdkApiProvider' import { toGuideRouteResultFromSgsPlan } from '@/data/adapters/sgsSdkRouteAdapter' import { stringifyId, formatSgsFloorLabel } from '@/data/adapters/sgsSdkGuideAdapter' const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({ ready: false, message, requiredData: ['route_graph', 'nav_data'] }) const logSdkRouteReadinessIssue = (message: string, payload: Record) => { console.warn(`[SDKRouteReadiness] ${message}`, payload) } const toNumber = (value: unknown) => ( Number.isFinite(Number(value)) ? Number(value) : undefined ) const isPositiveNumber = (value: unknown) => { const numberValue = toNumber(value) return typeof numberValue === 'number' && numberValue > 0 } const hasRouteNodeId = (place: SgsNavigablePlacePayload) => ( place.nodeId !== null && place.nodeId !== undefined && place.nodeId !== '' ) const hasRouteGraphEvidence = (floor: Pick) => ( floor.routePlanningReady === true || (isPositiveNumber(floor.routeNodeCount) && isPositiveNumber(floor.routeEdgeCount)) ) const hasRouteTargetEvidence = ( floor: Pick, routeTargetCount?: number ) => { if (typeof routeTargetCount === 'number') return routeTargetCount > 0 return floor.navigablePlaceCount === undefined || isPositiveNumber(floor.navigablePlaceCount) } const isRouteReadyFromDiagnostics = ( floor: Pick, routeTargetCount?: number ) => hasRouteGraphEvidence(floor) && hasRouteTargetEvidence(floor, routeTargetCount) const positionFromPlace = (place: SgsNavigablePlacePayload): [number, number, number] | undefined => { const source = place.position || { x: place.x, y: place.y, z: place.z } const x = toNumber(source.x) const z = toNumber(source.z) if (typeof x !== 'number' || typeof z !== 'number') return undefined return [ x, toNumber(source.y) || 0, z ] } export class SdkGuideRouteRepository { private targetsCache: GuideRouteTarget[] | null = null constructor( private readonly sdkApiProvider: SgsSdkApiProvider = defaultSgsSdkApiProvider ) {} async getRouteReadiness(): Promise { try { const diagnostics = await this.sdkApiProvider.getMapDiagnostics() const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor) if (!floors.length) { return createUnavailableReadiness('SDK 地图楼层数据为空') } // 地图级 diagnostics 的楼层摘要只保证 routePlanningReady/navigablePlaceCount; // routeNodeCount/routeEdgeCount 可能只出现在 floor diagnostics 或 summary 中。 let readyFloors = floors.filter((floor) => isRouteReadyFromDiagnostics(floor)) let notReadyFloors = floors.filter((floor) => !isRouteReadyFromDiagnostics(floor)) if (readyFloors.length === 0) { const floorEvidence = await Promise.all( floors.map((floor) => this.loadFloorRouteEvidence(floor)) ) readyFloors = floorEvidence .filter((item) => item.ready) .map((item) => item.floor) notReadyFloors = floorEvidence .filter((item) => !item.ready) .map((item) => item.floor) } // 没有楼层有路线数据时不可用 if (readyFloors.length === 0) { logSdkRouteReadinessIssue('SDK 路线数据未达到位置关系生成条件', { floorCount: floors.length, readyFloorCount: readyFloors.length, summary: diagnostics.summary, floors: floors.map((floor) => ({ floorId: floor.floorId, floorName: floor.floorName, floorCode: floor.floorCode, routePlanningReady: floor.routePlanningReady, routeNodeCount: floor.routeNodeCount, routeEdgeCount: floor.routeEdgeCount, navigablePlaceCount: floor.navigablePlaceCount })) }) return createUnavailableReadiness('无法定位,当前仅支持点位位置预览') } // 如果有楼层未就绪,给出警告但仍然可用 if (notReadyFloors.length > 0) { const floorLabels = notReadyFloors .map((f) => f.floorName || f.floorCode || String(f.floorId)) .join('、') return { ready: true, message: `部分楼层位置关系数据未就绪:${floorLabels},可查看已就绪楼层的位置关系`, requiredData: [] } } return { ready: true, message: 'SDK 位置关系数据已接入,可进行位置预览', requiredData: [] } } catch (error) { console.error('[SDKRouteReadiness] 读取 SDK 路线状态失败', error) return createUnavailableReadiness( error instanceof Error ? error.message : 'SDK 位置关系数据加载失败' ) } } private async loadFloorRouteEvidence(floor: SgsFloorDiagnosticsPayload) { const rawFloorId = stringifyId(floor.floorId) const [floorDiagnostics, navigablePlaces] = await Promise.all([ this.sdkApiProvider.getFloorDiagnostics(rawFloorId).catch(() => null), this.sdkApiProvider.getNavigablePlaces(rawFloorId).catch(() => []) ]) const routeTargetCount = navigablePlaces.filter(hasRouteNodeId).length const evidence = floorDiagnostics || floor return { floor: { ...floor, ...floorDiagnostics }, ready: isRouteReadyFromDiagnostics(evidence, routeTargetCount), routeTargetCount } } async listRouteTargets(): Promise { if (this.targetsCache) return this.targetsCache const diagnostics = await this.sdkApiProvider.getMapDiagnostics() const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor) const targets: GuideRouteTarget[] = [] for (const floor of floors) { // 使用原始 floorId 请求 API(SDK API 需要原始 ID 如 2065808921272119298) const rawFloorId = stringifyId(floor.floorId) try { const places = await this.sdkApiProvider.getNavigablePlaces(rawFloorId) for (const place of places) { // 过滤掉没有 nodeId 的点位,这些无法用于路线规划 if (!hasRouteNodeId(place)) { continue } const placeId = stringifyId(place.id) const placeName = place.name || `目的地 ${placeId}` const categoryLabel = place.category || place.typeName || place.typeCode || place.type || undefined const positionGltf = positionFromPlace(place) // 使用统一的楼层标签格式化函数,确保与 nav-assets 格式一致 const floorLabel = formatSgsFloorLabel( place.floorCode || undefined, place.floorName || floor.floorName || undefined ) targets.push({ poiId: placeId, name: placeName, // 保留后端原始 floorId,确保与 SDK 模型楼层和 ThreeMap 当前楼层一致。 floorId: stringifyId(place.floorId ?? rawFloorId), floorLabel, categoryLabel, positionGltf, // 保存原始 nodeId 用于 API 调用 routeNodeId: stringifyId(place.nodeId) }) } } catch { // 忽略单个楼层的加载错误 } } this.targetsCache = targets return targets } async findRoute(startPoiId: string, endPoiId: string): Promise { const readiness = await this.getRouteReadiness() if (!readiness.ready) { throw new GuideRouteError( 'ROUTE_DATA_UNAVAILABLE', readiness.message || NAV_ROUTE_READINESS.message ) } if (!startPoiId || !endPoiId) { throw new GuideRouteError('ROUTE_TARGET_MISSING', '请选择起点和终点') } if (startPoiId === endPoiId) { throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点和终点不能相同') } const targets = await this.listRouteTargets() const startTarget = targets.find((target) => target.poiId === startPoiId) const endTarget = targets.find((target) => target.poiId === endPoiId) if (!startTarget || !endTarget) { throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点或终点不在 SDK 可预览目的地列表中') } try { if (!startTarget.positionGltf || !endTarget.positionGltf) { throw new GuideRouteError('ROUTE_TARGET_UNANCHORED', '起点或终点缺少可规划坐标') } const route = await this.sdkApiProvider.planRoute({ startFloorId: startTarget.floorId, startX: startTarget.positionGltf[0], startY: startTarget.positionGltf[2], startNodeId: startTarget.routeNodeId || null, endFloorId: endTarget.floorId, endX: endTarget.positionGltf[0], endY: endTarget.positionGltf[2], endNodeId: endTarget.routeNodeId || null, wheelchair: 0 }) return toGuideRouteResultFromSgsPlan(route, startTarget, endTarget) } catch (error) { if (error instanceof GuideRouteError) throw error throw new GuideRouteError( 'ROUTE_NOT_FOUND', error instanceof Error ? error.message : 'SGS 位置关系接口生成失败' ) } } }