feat: 新增 SGS SDK 数据适配器与腾讯地图服务
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
415
src/data/adapters/sgsSdkGuideAdapter.ts
Normal file
415
src/data/adapters/sgsSdkGuideAdapter.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import type {
|
||||
GuideDataIntegrityIssue,
|
||||
GuideDataIntegrityReport,
|
||||
GuideDiagnosticsStatus,
|
||||
GuideFloorDetail,
|
||||
GuideLocationPreview,
|
||||
GuideMapDiagnostics,
|
||||
GuideRouteReadiness,
|
||||
MuseumCategory,
|
||||
MuseumFloor,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
} from '@/domain/guideReadiness'
|
||||
import type {
|
||||
SgsFloorDiagnosticsPayload,
|
||||
SgsMapDiagnosticsPayload,
|
||||
SgsPoiPayload,
|
||||
SgsPositionPayload,
|
||||
SgsSdkFloorSummaryPayload,
|
||||
SgsSdkManifestPayload
|
||||
} from '@/data/providers/sgsSdkApiProvider'
|
||||
|
||||
const defaultCategory: MuseumCategory = {
|
||||
id: 'operation_experience',
|
||||
label: '导览点位',
|
||||
iconType: 'poi'
|
||||
}
|
||||
|
||||
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
|
||||
toilet: {
|
||||
id: 'basic_service_facility',
|
||||
label: '卫生间',
|
||||
iconType: 'toilet'
|
||||
},
|
||||
accessible_toilet: {
|
||||
id: 'accessibility_special_service',
|
||||
label: '无障碍卫生间',
|
||||
iconType: 'accessible_toilet',
|
||||
accessible: true
|
||||
},
|
||||
elevator: {
|
||||
id: 'transport_circulation',
|
||||
label: '电梯',
|
||||
iconType: 'elevator',
|
||||
accessible: true
|
||||
},
|
||||
stairs: {
|
||||
id: 'transport_circulation',
|
||||
label: '楼梯',
|
||||
iconType: 'stairs'
|
||||
},
|
||||
escalator: {
|
||||
id: 'transport_circulation',
|
||||
label: '扶梯',
|
||||
iconType: 'escalator'
|
||||
},
|
||||
entrance_exit: {
|
||||
id: 'transport_circulation',
|
||||
label: '出入口',
|
||||
iconType: 'entrance_exit'
|
||||
},
|
||||
service_desk: {
|
||||
id: 'basic_service_facility',
|
||||
label: '服务台',
|
||||
iconType: 'service_desk'
|
||||
},
|
||||
mother_baby_room: {
|
||||
id: 'basic_service_facility',
|
||||
label: '母婴室',
|
||||
iconType: 'mother_baby_room',
|
||||
accessible: true
|
||||
},
|
||||
locker: {
|
||||
id: 'basic_service_facility',
|
||||
label: '存包处',
|
||||
iconType: 'locker'
|
||||
},
|
||||
rental_service: {
|
||||
id: 'basic_service_facility',
|
||||
label: '租赁服务',
|
||||
iconType: 'rental_service',
|
||||
accessible: true
|
||||
},
|
||||
ticket_office: {
|
||||
id: 'basic_service_facility',
|
||||
label: '售票处',
|
||||
iconType: 'ticket_office'
|
||||
}
|
||||
}
|
||||
|
||||
export const stringifyId = (value: string | number | null | undefined) => (
|
||||
value === null || typeof value === 'undefined' ? '' : String(value)
|
||||
)
|
||||
|
||||
const toNumber = (value: number | null | undefined, fallback = 0) => (
|
||||
Number.isFinite(Number(value)) ? Number(value) : fallback
|
||||
)
|
||||
|
||||
const optionalNumber = (value: number | null | undefined) => (
|
||||
Number.isFinite(Number(value)) ? Number(value) : undefined
|
||||
)
|
||||
|
||||
const normalizeStatus = (status?: string): GuideDiagnosticsStatus => {
|
||||
if (status === 'OK' || status === 'WARN' || status === 'ERROR') return status
|
||||
return 'WARN'
|
||||
}
|
||||
|
||||
export const formatSgsFloorLabel = (floorCode?: string | null, floorName?: string | null) => {
|
||||
if (floorCode === 'EXTERIOR') return '室外'
|
||||
|
||||
const basementMatch = floorCode?.match(/^L-(\d+(?:\.\d+)?)$/)
|
||||
if (basementMatch) return `B${basementMatch[1]}`
|
||||
|
||||
const floorMatch = floorCode?.match(/^L(\d+(?:\.\d+)?)$/)
|
||||
if (floorMatch) return `${floorMatch[1]}F`
|
||||
|
||||
return floorName || floorCode || '未知楼层'
|
||||
}
|
||||
|
||||
export const toMuseumFloorFromSgs = (floor: SgsSdkFloorSummaryPayload): MuseumFloor => ({
|
||||
id: stringifyId(floor.floorId),
|
||||
label: formatSgsFloorLabel(floor.floorCode, floor.floorName),
|
||||
order: toNumber(floor.sortOrder)
|
||||
})
|
||||
|
||||
export const buildSgsFloorAliases = (floors: SgsSdkFloorSummaryPayload[]) => {
|
||||
const aliases = new Map<string, string>()
|
||||
|
||||
floors.forEach((floor) => {
|
||||
const floorId = stringifyId(floor.floorId)
|
||||
if (!floorId) return
|
||||
|
||||
const label = formatSgsFloorLabel(floor.floorCode, floor.floorName)
|
||||
aliases.set(floorId, floorId)
|
||||
if (floor.floorCode) aliases.set(String(floor.floorCode), floorId)
|
||||
if (floor.floorName) aliases.set(String(floor.floorName), floorId)
|
||||
aliases.set(label, floorId)
|
||||
aliases.set(label.toLowerCase(), floorId)
|
||||
})
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
const normalizePosition = (poi: SgsPoiPayload): [number, number, number] | undefined => {
|
||||
const source: SgsPositionPayload = poi.position || {
|
||||
x: poi.x,
|
||||
y: poi.y,
|
||||
z: poi.z
|
||||
}
|
||||
const x = Number(source.x)
|
||||
const y = Number(source.y)
|
||||
const z = Number(source.z)
|
||||
|
||||
if (![x, y, z].every(Number.isFinite)) return undefined
|
||||
return [x, y, z]
|
||||
}
|
||||
|
||||
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
|
||||
const normalizedType = poi.type?.trim()
|
||||
if (normalizedType && categoryBySgsType[normalizedType]) {
|
||||
return categoryBySgsType[normalizedType]
|
||||
}
|
||||
|
||||
if (poi.typeName) {
|
||||
return {
|
||||
...defaultCategory,
|
||||
label: poi.typeName,
|
||||
iconType: normalizedType || defaultCategory.iconType
|
||||
}
|
||||
}
|
||||
|
||||
return defaultCategory
|
||||
}
|
||||
|
||||
export const toMuseumPoiFromSgs = (
|
||||
poi: SgsPoiPayload,
|
||||
floors: SgsSdkFloorSummaryPayload[]
|
||||
): MuseumPoi => {
|
||||
const floorId = stringifyId(poi.floorId)
|
||||
const matchedFloor = floors.find((floor) => stringifyId(floor.floorId) === floorId || floor.floorCode === poi.floorCode)
|
||||
const category = categoryFor(poi)
|
||||
|
||||
return {
|
||||
id: stringifyId(poi.id),
|
||||
name: poi.name?.trim() || `未命名点位 ${stringifyId(poi.id)}`,
|
||||
floorId,
|
||||
floorLabel: formatSgsFloorLabel(matchedFloor?.floorCode || poi.floorCode, matchedFloor?.floorName),
|
||||
primaryCategory: {
|
||||
id: category.id,
|
||||
label: category.label,
|
||||
iconType: category.iconType
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: category.id,
|
||||
label: category.label,
|
||||
iconType: category.iconType
|
||||
}
|
||||
],
|
||||
positionGltf: normalizePosition(poi),
|
||||
sourceObjectName: poi.anchorNodeName || undefined,
|
||||
sourceConfidence: 'backend-sgs-sdk',
|
||||
navigationReadiness: '位置预览',
|
||||
accessible: category.accessible === true
|
||||
}
|
||||
}
|
||||
|
||||
export const toLocationPreviewFromPoi = (poi: MuseumPoi): GuideLocationPreview => ({
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
primaryCategoryZh: poi.primaryCategory.label,
|
||||
positionGltf: poi.positionGltf,
|
||||
sourceObjectName: poi.sourceObjectName
|
||||
})
|
||||
|
||||
export const toGuideFloorDetailFromSgs = (
|
||||
floor: SgsSdkFloorSummaryPayload | SgsFloorDiagnosticsPayload
|
||||
): GuideFloorDetail => ({
|
||||
...toMuseumFloorFromSgs(floor),
|
||||
sourceCode: floor.floorCode || undefined,
|
||||
sourceName: floor.floorName || undefined,
|
||||
modelReady: 'modelReady' in floor ? floor.modelReady === true : undefined,
|
||||
poiCount: optionalNumber(floor.poiCount),
|
||||
spaceCount: optionalNumber(floor.spaceCount),
|
||||
guideStopCount: 'guideStopCount' in floor ? toNumber(floor.guideStopCount, 0) : undefined,
|
||||
navigablePlaceCount: 'navigablePlaceCount' in floor ? toNumber(floor.navigablePlaceCount, 0) : undefined,
|
||||
routeNodeCount: 'routeNodeCount' in floor ? toNumber(floor.routeNodeCount, 0) : undefined,
|
||||
routeEdgeCount: 'routeEdgeCount' in floor ? toNumber(floor.routeEdgeCount, 0) : undefined,
|
||||
routePlanningReady: 'routePlanningReady' in floor ? floor.routePlanningReady === true : undefined,
|
||||
warnings: 'warnings' in floor ? floor.warnings || [] : []
|
||||
})
|
||||
|
||||
export const toGuideMapDiagnostics = (
|
||||
diagnostics: SgsMapDiagnosticsPayload,
|
||||
manifest?: SgsSdkManifestPayload
|
||||
): GuideMapDiagnostics => {
|
||||
const summary = diagnostics.summary || {}
|
||||
const floors = diagnostics.floors?.length
|
||||
? diagnostics.floors.map(toGuideFloorDetailFromSgs)
|
||||
: (manifest?.floors || []).map(toGuideFloorDetailFromSgs)
|
||||
|
||||
return {
|
||||
mapId: stringifyId(diagnostics.mapId || manifest?.mapId),
|
||||
mapName: diagnostics.mapName || manifest?.mapName || 'SGS 地图',
|
||||
sdkVersion: diagnostics.sdkVersion || manifest?.sdkVersion || undefined,
|
||||
status: normalizeStatus(diagnostics.status),
|
||||
floors,
|
||||
summary: {
|
||||
floorCount: toNumber(summary.floorCount, floors.length),
|
||||
modelReadyFloorCount: toNumber(summary.modelReadyFloorCount),
|
||||
poiCount: toNumber(summary.poiCount),
|
||||
spaceCount: toNumber(summary.spaceCount),
|
||||
guideStopCount: toNumber(summary.guideStopCount),
|
||||
navigablePlaceCount: toNumber(summary.navigablePlaceCount),
|
||||
routeNodeCount: toNumber(summary.routeNodeCount),
|
||||
routeEdgeCount: toNumber(summary.routeEdgeCount)
|
||||
},
|
||||
warnings: diagnostics.warnings || []
|
||||
}
|
||||
}
|
||||
|
||||
export const toRouteReadinessFromSgsDiagnostics = (
|
||||
diagnostics: SgsMapDiagnosticsPayload
|
||||
): GuideRouteReadiness => {
|
||||
const warnings = diagnostics.warnings || []
|
||||
const floors = diagnostics.floors || []
|
||||
const notReadyFloors = floors.filter((floor) => (
|
||||
floor.modelReady !== true
|
||||
|| floor.routePlanningReady !== true
|
||||
|| toNumber(floor.routeNodeCount) <= 0
|
||||
|| toNumber(floor.routeEdgeCount) <= 0
|
||||
|| toNumber(floor.navigablePlaceCount) <= 0
|
||||
))
|
||||
|
||||
const ready = diagnostics.status === 'OK' && notReadyFloors.length === 0 && warnings.length === 0
|
||||
|
||||
return {
|
||||
ready,
|
||||
message: ready
|
||||
? 'SGS 后端路线数据已就绪'
|
||||
: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
requiredData: ready
|
||||
? []
|
||||
: [
|
||||
...warnings,
|
||||
...notReadyFloors.map((floor) => `${formatSgsFloorLabel(floor.floorCode, floor.floorName)} 数据未完全就绪`)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const addIssue = (
|
||||
issues: GuideDataIntegrityIssue[],
|
||||
issue: Omit<GuideDataIntegrityIssue, 'id'>
|
||||
) => {
|
||||
issues.push({
|
||||
id: `issue-${issues.length + 1}`,
|
||||
...issue
|
||||
})
|
||||
}
|
||||
|
||||
const missingFieldsForPoi = (poi: MuseumPoi) => {
|
||||
const fields: string[] = []
|
||||
if (!poi.id) fields.push('id')
|
||||
if (!poi.name) fields.push('name')
|
||||
if (!poi.floorId) fields.push('floorId')
|
||||
if (!poi.positionGltf) fields.push('position')
|
||||
return fields
|
||||
}
|
||||
|
||||
export const createGuideDataIntegrityReport = (
|
||||
diagnostics: GuideMapDiagnostics,
|
||||
floors: MuseumFloor[],
|
||||
pois: MuseumPoi[]
|
||||
): GuideDataIntegrityReport => {
|
||||
const issues: GuideDataIntegrityIssue[] = []
|
||||
const floorIds = new Set(floors.map((floor) => floor.id))
|
||||
const poiIds = new Set<string>()
|
||||
|
||||
floors.forEach((floor) => {
|
||||
if (!floor.id || !floor.label) {
|
||||
addIssue(issues, {
|
||||
scope: 'floor',
|
||||
severity: 'error',
|
||||
message: '楼层缺少稳定 ID 或显示名称',
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
fields: ['id', 'label'].filter((field) => !floor[field as keyof MuseumFloor])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
diagnostics.floors.forEach((floor) => {
|
||||
if (floor.modelReady === false) {
|
||||
addIssue(issues, {
|
||||
scope: 'floor',
|
||||
severity: 'warn',
|
||||
message: `${floor.label} 模型未就绪`,
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
fields: ['modelReady']
|
||||
})
|
||||
}
|
||||
|
||||
if (floor.navigablePlaceCount === 0) {
|
||||
addIssue(issues, {
|
||||
scope: 'floor',
|
||||
severity: 'warn',
|
||||
message: `${floor.label} 缺少可导航目的地`,
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
fields: ['navigablePlaceCount']
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
pois.forEach((poi) => {
|
||||
const missingFields = missingFieldsForPoi(poi)
|
||||
if (missingFields.length) {
|
||||
addIssue(issues, {
|
||||
scope: 'poi',
|
||||
severity: 'error',
|
||||
message: `${poi.name || poi.id || '未知点位'} 缺少必要字段`,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
poiId: poi.id,
|
||||
fields: missingFields
|
||||
})
|
||||
}
|
||||
|
||||
if (poi.id && poiIds.has(poi.id)) {
|
||||
addIssue(issues, {
|
||||
scope: 'poi',
|
||||
severity: 'error',
|
||||
message: `${poi.name || poi.id} 点位 ID 重复`,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
poiId: poi.id,
|
||||
fields: ['id']
|
||||
})
|
||||
}
|
||||
if (poi.id) poiIds.add(poi.id)
|
||||
|
||||
if (poi.floorId && !floorIds.has(poi.floorId)) {
|
||||
addIssue(issues, {
|
||||
scope: 'poi',
|
||||
severity: 'error',
|
||||
message: `${poi.name || poi.id} 关联的楼层不存在`,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
poiId: poi.id,
|
||||
fields: ['floorId']
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const errorCount = issues.filter((issue) => issue.severity === 'error').length
|
||||
const warningCount = issues.filter((issue) => issue.severity === 'warn').length
|
||||
|
||||
return {
|
||||
status: errorCount > 0 ? 'ERROR' : warningCount > 0 || diagnostics.status === 'WARN' ? 'WARN' : 'OK',
|
||||
summary: {
|
||||
floorCount: floors.length,
|
||||
poiCount: pois.length,
|
||||
issueCount: issues.length,
|
||||
errorCount,
|
||||
warningCount
|
||||
},
|
||||
issues,
|
||||
warnings: diagnostics.warnings
|
||||
}
|
||||
}
|
||||
318
src/data/adapters/sgsSdkRouteAdapter.ts
Normal file
318
src/data/adapters/sgsSdkRouteAdapter.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import type {
|
||||
GuideRouteConnectorPoint,
|
||||
GuideRouteEndpoint,
|
||||
GuideRouteFloorSegment,
|
||||
GuideRoutePoint,
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget
|
||||
} from '@/domain/museum'
|
||||
import type {
|
||||
SgsRoutePlanResponsePayload
|
||||
} from '@/data/providers/sgsSdkApiProvider'
|
||||
import type {
|
||||
SgsRouteResult as SgsSdkRuntimeRouteResult
|
||||
} from '@/types/sgs-map-sdk'
|
||||
import { toAppFloorId } from '@/services/sgs/SgsMapEventAdapter'
|
||||
|
||||
type SgsRoutePathNode = {
|
||||
nodeId?: string | number
|
||||
floorId?: string | number
|
||||
x: number
|
||||
y?: number
|
||||
z: number
|
||||
}
|
||||
|
||||
type CompatibleSgsRouteResult = SgsSdkRuntimeRouteResult & {
|
||||
path?: SgsRoutePathNode[]
|
||||
}
|
||||
|
||||
type GeoJsonLineString = {
|
||||
type?: string
|
||||
coordinates?: Array<[number, number] | [number, number, number]>
|
||||
}
|
||||
|
||||
const pointPosition = (point: SgsRoutePathNode): [number, number, number] => [
|
||||
Number(point.x || 0),
|
||||
Number(point.y || 0),
|
||||
Number(point.z || 0)
|
||||
]
|
||||
|
||||
const targetEndpoint = (target: GuideRouteTarget): GuideRouteEndpoint => ({
|
||||
poiId: target.poiId,
|
||||
name: target.name,
|
||||
floorId: target.floorId,
|
||||
floorLabel: target.floorLabel,
|
||||
routeNodeId: target.routeNodeId,
|
||||
position: target.positionGltf || [0, 0, 0]
|
||||
})
|
||||
|
||||
const normalizePath = (
|
||||
route: CompatibleSgsRouteResult,
|
||||
startFloorId: string,
|
||||
endFloorId: string
|
||||
): GuideRoutePoint[] => {
|
||||
const pathPoints = route.pathPoints ?? route.path ?? []
|
||||
|
||||
if (!pathPoints.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (route.path?.length) {
|
||||
return route.path.map((point, index) => ({
|
||||
nodeId: String(point.nodeId || `sdk-route-point-${index}`),
|
||||
floorId: toAppFloorId(point.floorId ?? startFloorId),
|
||||
position: pointPosition(point)
|
||||
}))
|
||||
}
|
||||
|
||||
const isCrossFloor = startFloorId !== endFloorId
|
||||
const totalPoints = pathPoints.length
|
||||
const normalizedStartFloorId = toAppFloorId(startFloorId)
|
||||
const normalizedEndFloorId = toAppFloorId(endFloorId)
|
||||
|
||||
return pathPoints.map((point, index) => {
|
||||
const nodeId = `sdk-pp-${index}`
|
||||
let floorId: string
|
||||
|
||||
if (isCrossFloor) {
|
||||
if (index === 0) {
|
||||
floorId = normalizedStartFloorId
|
||||
} else if (index === totalPoints - 1) {
|
||||
floorId = normalizedEndFloorId
|
||||
} else {
|
||||
floorId = normalizedStartFloorId
|
||||
}
|
||||
} else {
|
||||
floorId = normalizedStartFloorId
|
||||
}
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
floorId,
|
||||
position: pointPosition(point)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createFloorSegments = (
|
||||
points: GuideRoutePoint[],
|
||||
start: GuideRouteTarget,
|
||||
end: GuideRouteTarget
|
||||
): GuideRouteFloorSegment[] => {
|
||||
const normalizedStartFloorId = toAppFloorId(start.floorId)
|
||||
const normalizedEndFloorId = toAppFloorId(end.floorId)
|
||||
|
||||
const labels = new Map([
|
||||
[normalizedStartFloorId, start.floorLabel],
|
||||
[normalizedEndFloorId, end.floorLabel]
|
||||
])
|
||||
|
||||
return points.reduce<GuideRouteFloorSegment[]>((segments, point) => {
|
||||
const current = segments[segments.length - 1]
|
||||
if (current && current.floorId === point.floorId) {
|
||||
current.points.push(point)
|
||||
return segments
|
||||
}
|
||||
|
||||
segments.push({
|
||||
floorId: point.floorId,
|
||||
floorLabel: labels.get(point.floorId) || point.floorId,
|
||||
points: [point]
|
||||
})
|
||||
return segments
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const toGuideRouteResultFromSgs = (
|
||||
route: SgsSdkRuntimeRouteResult,
|
||||
startTarget: GuideRouteTarget,
|
||||
endTarget: GuideRouteTarget
|
||||
): GuideRouteResult => {
|
||||
const start = targetEndpoint(startTarget)
|
||||
const end = targetEndpoint(endTarget)
|
||||
const points = normalizePath(
|
||||
route as CompatibleSgsRouteResult,
|
||||
startTarget.floorId,
|
||||
endTarget.floorId
|
||||
)
|
||||
const routePoints = points.length
|
||||
? points
|
||||
: [
|
||||
{
|
||||
nodeId: start.routeNodeId,
|
||||
floorId: start.floorId,
|
||||
position: start.position
|
||||
},
|
||||
{
|
||||
nodeId: end.routeNodeId,
|
||||
floorId: end.floorId,
|
||||
position: end.position
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
id: `sdk-route-${start.poiId}-${end.poiId}`,
|
||||
start,
|
||||
end,
|
||||
distanceMeters: Number(route.distance || 0),
|
||||
nodeIds: routePoints.map((point) => point.nodeId),
|
||||
points: routePoints,
|
||||
floorSegments: createFloorSegments(routePoints, startTarget, endTarget),
|
||||
connectorPoints: []
|
||||
}
|
||||
}
|
||||
|
||||
const floorLabelFor = (
|
||||
floorId: string,
|
||||
startTarget: GuideRouteTarget,
|
||||
endTarget: GuideRouteTarget
|
||||
) => {
|
||||
if (floorId === startTarget.floorId) return startTarget.floorLabel
|
||||
if (floorId === endTarget.floorId) return endTarget.floorLabel
|
||||
return floorId
|
||||
}
|
||||
|
||||
const pointFromBackendNode = (
|
||||
node: NonNullable<SgsRoutePlanResponsePayload['nodePaths']>[number],
|
||||
index: number,
|
||||
fallbackFloorId: string
|
||||
): GuideRoutePoint => ({
|
||||
nodeId: String(node.id || `sgs-route-node-${index}`),
|
||||
floorId: toAppFloorId(node.floorId ?? fallbackFloorId),
|
||||
position: [
|
||||
Number(node.x || 0),
|
||||
0,
|
||||
Number(node.y || 0)
|
||||
]
|
||||
})
|
||||
|
||||
const pointsFromGeoJson = (
|
||||
geoJson: string | null | undefined,
|
||||
floorId: string
|
||||
): GuideRoutePoint[] => {
|
||||
if (!geoJson) return []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(geoJson) as GeoJsonLineString
|
||||
const coordinates = parsed.type === 'LineString' && Array.isArray(parsed.coordinates)
|
||||
? parsed.coordinates
|
||||
: []
|
||||
|
||||
return coordinates.map((coordinate, index) => ({
|
||||
nodeId: `${floorId}-geo-${index}`,
|
||||
floorId,
|
||||
position: [
|
||||
Number(coordinate[0] || 0),
|
||||
0,
|
||||
Number(coordinate[1] || 0)
|
||||
]
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const toGuideRouteResultFromSgsPlan = (
|
||||
route: SgsRoutePlanResponsePayload,
|
||||
startTarget: GuideRouteTarget,
|
||||
endTarget: GuideRouteTarget
|
||||
): GuideRouteResult => {
|
||||
const start = targetEndpoint(startTarget)
|
||||
const end = targetEndpoint(endTarget)
|
||||
const nodePoints = (route.nodePaths || []).map((node, index) => (
|
||||
pointFromBackendNode(node, index, start.floorId)
|
||||
))
|
||||
const routePoints = nodePoints.length
|
||||
? [
|
||||
{
|
||||
nodeId: start.routeNodeId,
|
||||
floorId: start.floorId,
|
||||
position: start.position
|
||||
},
|
||||
...nodePoints,
|
||||
{
|
||||
nodeId: end.routeNodeId,
|
||||
floorId: end.floorId,
|
||||
position: end.position
|
||||
}
|
||||
]
|
||||
: pointsFromGeoJson(route.pathGeoJson, start.floorId)
|
||||
|
||||
const fallbackPoints = routePoints.length
|
||||
? routePoints
|
||||
: [
|
||||
{
|
||||
nodeId: start.routeNodeId,
|
||||
floorId: start.floorId,
|
||||
position: start.position
|
||||
},
|
||||
{
|
||||
nodeId: end.routeNodeId,
|
||||
floorId: end.floorId,
|
||||
position: end.position
|
||||
}
|
||||
]
|
||||
|
||||
const floorSegments = route.segments?.length
|
||||
? route.segments.map((segment, segmentIndex) => {
|
||||
const floorId = String(segment.floorId || start.floorId)
|
||||
const points = pointsFromGeoJson(segment.pathGeoJson, floorId)
|
||||
|
||||
return {
|
||||
floorId,
|
||||
floorLabel: segment.floorName || floorLabelFor(floorId, startTarget, endTarget),
|
||||
points: points.length
|
||||
? points
|
||||
: fallbackPoints.filter((point) => point.floorId === floorId).map((point, index) => ({
|
||||
...point,
|
||||
nodeId: point.nodeId || `${floorId}-segment-${segmentIndex}-${index}`
|
||||
}))
|
||||
}
|
||||
}).filter((segment) => segment.points.length)
|
||||
: createFloorSegments(fallbackPoints, startTarget, endTarget)
|
||||
|
||||
return {
|
||||
id: `sgs-api-route-${start.poiId}-${end.poiId}`,
|
||||
start,
|
||||
end,
|
||||
distanceMeters: Number(route.distance || 0),
|
||||
nodeIds: fallbackPoints.map((point) => point.nodeId),
|
||||
points: fallbackPoints,
|
||||
floorSegments,
|
||||
connectorPoints: extractFloorConnectorPoints(fallbackPoints, startTarget, endTarget)
|
||||
}
|
||||
}
|
||||
|
||||
const extractFloorConnectorPoints = (
|
||||
points: GuideRoutePoint[],
|
||||
startTarget: GuideRouteTarget,
|
||||
endTarget: GuideRouteTarget
|
||||
): GuideRouteConnectorPoint[] => {
|
||||
if (points.length < 2) return []
|
||||
|
||||
const connectors: GuideRouteConnectorPoint[] = []
|
||||
const labels = new Map<string, string>([
|
||||
[startTarget.floorId, startTarget.floorLabel],
|
||||
[endTarget.floorId, endTarget.floorLabel]
|
||||
])
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const currentFloorId = toAppFloorId(points[i].floorId)
|
||||
const nextFloorId = toAppFloorId(points[i + 1].floorId)
|
||||
|
||||
if (currentFloorId !== nextFloorId) {
|
||||
const fromLabel = labels.get(currentFloorId) || currentFloorId
|
||||
const toLabel = labels.get(nextFloorId) || nextFloorId
|
||||
|
||||
connectors.push({
|
||||
nodeId: `connector-${points[i].nodeId}-${nextFloorId}`,
|
||||
floorId: nextFloorId,
|
||||
floorLabel: `经 ${fromLabel} 至 ${toLabel}`,
|
||||
position: points[i].position,
|
||||
connectorType: 'floor_change'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return connectors
|
||||
}
|
||||
Reference in New Issue
Block a user