226
src/repositories/SdkGuideRouteRepository.ts
Normal file
226
src/repositories/SdkGuideRouteRepository.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import type {
|
||||
GuideRouteReadiness,
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
GuideRouteError
|
||||
} from '@/repositories/GuideRouteRepository'
|
||||
import {
|
||||
defaultSgsSdkApiProvider,
|
||||
type SgsNavigablePlacePayload,
|
||||
type SgsSdkApiProvider
|
||||
} from '@/data/providers/sgsSdkApiProvider'
|
||||
import {
|
||||
toGuideRouteResultFromSgsPlan
|
||||
} from '@/data/adapters/sgsSdkRouteAdapter'
|
||||
import { stringifyId, formatSgsFloorLabel } from '@/data/adapters/sgsSdkGuideAdapter'
|
||||
import { toAppFloorId } from '@/services/sgs/SgsMapEventAdapter'
|
||||
|
||||
const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
|
||||
ready: false,
|
||||
message,
|
||||
requiredData: ['route_graph', 'nav_data']
|
||||
})
|
||||
|
||||
const toNumber = (value: unknown) => (
|
||||
Number.isFinite(Number(value)) ? Number(value) : undefined
|
||||
)
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
const numericId = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : undefined
|
||||
}
|
||||
|
||||
export class SdkGuideRouteRepository {
|
||||
private targetsCache: GuideRouteTarget[] | null = null
|
||||
|
||||
constructor(
|
||||
private readonly sdkApiProvider: SgsSdkApiProvider = defaultSgsSdkApiProvider
|
||||
) {}
|
||||
|
||||
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
||||
try {
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floors = diagnostics.floors || []
|
||||
|
||||
if (!floors.length) {
|
||||
return createUnavailableReadiness('SDK 地图楼层数据为空')
|
||||
}
|
||||
|
||||
// 收集有路线数据的楼层
|
||||
const readyFloors = floors.filter(
|
||||
(floor) =>
|
||||
floor.routePlanningReady &&
|
||||
floor.routeNodeCount &&
|
||||
floor.routeNodeCount > 0 &&
|
||||
floor.routeEdgeCount &&
|
||||
floor.routeEdgeCount > 0
|
||||
)
|
||||
|
||||
// 没有楼层有路线数据时不可用
|
||||
if (readyFloors.length === 0) {
|
||||
return createUnavailableReadiness('SDK 路线规划数据不完整,当前暂不支持正式导航')
|
||||
}
|
||||
|
||||
// 收集未就绪的楼层
|
||||
const notReadyFloors = floors.filter((floor) => !floor.routePlanningReady)
|
||||
|
||||
// 如果有楼层未就绪,给出警告但仍然可用
|
||||
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) {
|
||||
return createUnavailableReadiness(
|
||||
error instanceof Error ? error.message : 'SDK 路线数据加载失败'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async listRouteTargets(): Promise<GuideRouteTarget[]> {
|
||||
if (this.targetsCache) return this.targetsCache
|
||||
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floors = diagnostics.floors || []
|
||||
|
||||
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 (place.nodeId === null || place.nodeId === undefined || place.nodeId === '') {
|
||||
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(如 L1)
|
||||
floorId: toAppFloorId(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<GuideRouteResult> {
|
||||
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', '起点或终点缺少可规划坐标')
|
||||
}
|
||||
|
||||
// 获取原始 floorId 用于 API 调用
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floorIdMap = new Map<string, string>()
|
||||
for (const floor of diagnostics.floors || []) {
|
||||
floorIdMap.set(toAppFloorId(floor.floorId), stringifyId(floor.floorId))
|
||||
}
|
||||
const rawStartFloorId = floorIdMap.get(startTarget.floorId) || startTarget.floorId
|
||||
const rawEndFloorId = floorIdMap.get(endTarget.floorId) || endTarget.floorId
|
||||
|
||||
const route = await this.sdkApiProvider.planRoute({
|
||||
startFloorId: rawStartFloorId,
|
||||
startX: startTarget.positionGltf[0],
|
||||
startY: startTarget.positionGltf[2],
|
||||
startNodeId: numericId(startTarget.routeNodeId) || null,
|
||||
endFloorId: rawEndFloorId,
|
||||
endX: endTarget.positionGltf[0],
|
||||
endY: endTarget.positionGltf[2],
|
||||
endNodeId: numericId(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 路线接口规划失败'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user