feat: expand SGS hall POI adaptation

This commit is contained in:
lyf
2026-06-30 01:01:41 +08:00
parent e826c92486
commit c06d309e08
4 changed files with 1311 additions and 12 deletions

View File

@@ -19,10 +19,12 @@ import {
import type {
SgsFloorDiagnosticsPayload,
SgsMapDiagnosticsPayload,
SgsNavigablePlacePayload,
SgsPoiPayload,
SgsPositionPayload,
SgsSdkFloorSummaryPayload,
SgsSdkManifestPayload
SgsSdkManifestPayload,
SgsSpacePayload
} from '@/data/providers/sgsSdkApiProvider'
const defaultCategory: MuseumCategory = {
@@ -31,6 +33,18 @@ const defaultCategory: MuseumCategory = {
iconType: 'poi'
}
const hallCategory: MuseumCategory = {
id: 'exhibition_hall',
label: '展厅',
iconType: 'exhibition_hall'
}
const hallEntranceCategory: MuseumCategory = {
id: 'exhibition_hall_entrance',
label: '展厅出入口',
iconType: 'hall_entrance'
}
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
toilet: {
id: 'basic_service_facility',
@@ -105,13 +119,24 @@ const optionalNumber = (value: number | null | undefined) => (
Number.isFinite(Number(value)) ? Number(value) : undefined
)
const normalizedText = (value?: string | null) => (value || '').trim()
const normalizePositionSource = (source: SgsPositionPayload): [number, number, number] | undefined => {
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 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 '外'
if (floorCode === 'EXTERIOR') return '外'
const basementMatch = floorCode?.match(/^L-(\d+(?:\.\d+)?)$/)
if (basementMatch) return `B${basementMatch[1]}`
@@ -148,19 +173,119 @@ export const buildSgsFloorAliases = (floors: SgsSdkFloorSummaryPayload[]) => {
return aliases
}
const normalizePosition = (poi: SgsPoiPayload): [number, number, number] | undefined => {
const source: SgsPositionPayload = poi.position || {
const exhibitionSpaceTypeWhitelist = new Set([
'exhibition_hall',
'theater',
'education_activity',
'ramp'
])
const exhibitionHallKeywords = [
'展厅',
'临展',
'展览',
'影院',
'球幕',
'巨幕',
'报告厅',
'活动室',
'科普',
'实验室',
'展览坡道',
'宇宙',
'地球',
'演化',
'恐龙',
'人类',
'生物',
'生态',
'家园'
]
export interface SgsHallPoiDiagnostics {
spaceCount: number
eligibleSpaceCount: number
navigablePlaceCount: number
hallPlaceCount: number
hallPoiCount: number
hallPoiWithPositionCount: number
skippedSpaceCount: number
skippedPlaceCount: number
}
const hasExhibitionKeyword = (value?: string | null) => (
exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword))
)
const searchableSpaceText = (space: SgsSpacePayload) => [
space.name,
space.type,
space.sourceNodeName
]
.map((value) => normalizedText(value))
.filter(Boolean)
.join(' ')
const normalizeSpaceCenter = (space: SgsSpacePayload): [number, number, number] | undefined => normalizePositionSource(
space.center || {
x: undefined,
y: undefined,
z: undefined
}
)
export const isExhibitionHallSpace = (space: SgsSpacePayload) => {
const type = normalizedText(space.type).toLowerCase()
const name = normalizedText(space.name)
const searchableText = searchableSpaceText(space)
if (!exhibitionSpaceTypeWhitelist.has(type)) return false
if (type === 'exhibition_hall') return true
if (type === 'ramp') return name.includes('展览坡道')
return hasExhibitionKeyword(searchableText)
}
export const isExhibitionHallNavigablePlace = (place: SgsNavigablePlacePayload) => {
const searchableText = [
place.name,
place.ownerName,
place.category,
place.type,
place.typeCode,
place.typeName
]
.map((value) => normalizedText(value))
.filter(Boolean)
.join(' ')
return hasExhibitionKeyword(searchableText)
}
const normalizedHallName = (value?: string | null) => normalizedText(value)
.replace(/[\s()【】\[\]_-]/g, '')
const normalizePosition = (poi: SgsPoiPayload): [number, number, number] | undefined => normalizePositionSource(
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 normalizePlacePosition = (place: SgsNavigablePlacePayload): [number, number, number] | undefined => normalizePositionSource(
place.position
? {
x: place.position.x,
y: place.position.y ?? 0,
z: place.position.z
}
: {
x: place.x,
y: place.y ?? 0,
z: place.z
}
)
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
const normalizedType = poi.type?.trim()
@@ -208,7 +333,8 @@ export const toMuseumPoiFromSgs = (
sourceObjectName: poi.anchorNodeName || undefined,
sourceConfidence: 'backend-sgs-sdk',
navigationReadiness: '位置预览',
accessible: category.accessible === true
accessible: category.accessible === true,
kind: category.id === 'operation_experience' ? 'guide' : 'facility'
}
}
@@ -219,9 +345,229 @@ export const toLocationPreviewFromPoi = (poi: MuseumPoi): GuideLocationPreview =
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName
sourceObjectName: poi.sourceObjectName,
kind: poi.kind,
hallId: poi.hallId,
hallName: poi.hallName,
entrances: poi.entrances
})
const placeHallName = (place: SgsNavigablePlacePayload) => (
normalizedText(place.ownerName)
|| normalizedText(place.name)
.replace(/(?:)?(?:|||)\s*[A-Za-z0-9-]*$/u, '')
.trim()
|| normalizedText(place.name)
)
const findMatchedHallSpace = (
place: SgsNavigablePlacePayload,
spaces: SgsSpacePayload[]
) => {
const placeName = normalizedHallName(placeHallName(place))
if (!placeName) return undefined
return spaces.find((space) => {
const spaceName = normalizedHallName(space.name)
return spaceName && (placeName.includes(spaceName) || spaceName.includes(placeName))
})
}
const floorLabelForSgs = (
floorId: string,
floors: SgsSdkFloorSummaryPayload[],
floorCode?: string | null,
floorName?: string | null
) => {
const matchedFloor = floors.find((floor) => stringifyId(floor.floorId) === floorId || floor.floorCode === floorCode)
return formatSgsFloorLabel(
matchedFloor?.floorCode || floorCode,
matchedFloor?.floorName || floorName
)
}
const createHallEntrance = (
place: SgsNavigablePlacePayload,
floors: SgsSdkFloorSummaryPayload[],
fallbackFloorId: string
) => {
const floorId = stringifyId(place.floorId) || fallbackFloorId
const placeId = stringifyId(place.id || place.nodeId)
return {
id: placeId ? `hall-entrance-${placeId}` : `hall-entrance-${floorId}-${normalizedHallName(place.name)}`,
name: normalizedText(place.name) || normalizedText(place.ownerName) || '展厅出入口',
floorId,
floorLabel: floorLabelForSgs(floorId, floors, place.floorCode, place.floorName),
positionGltf: normalizePlacePosition(place),
sourceObjectName: stringifyId(place.nodeId) || undefined,
sourcePlaceId: placeId || undefined,
routeNodeId: stringifyId(place.nodeId) || undefined
}
}
const createHallPoiId = (
space: SgsSpacePayload | undefined,
place: SgsNavigablePlacePayload,
fallbackIndex: number
) => {
const spaceId = stringifyId(space?.id)
if (spaceId) return `hall-${spaceId}`
const ownerName = normalizedHallName(placeHallName(place))
if (ownerName) return `hall-place-${ownerName}`
return `hall-place-${stringifyId(place.id) || fallbackIndex + 1}`
}
const createSpaceFallbackHallPoi = (
space: SgsSpacePayload,
floors: SgsSdkFloorSummaryPayload[],
fallbackFloorId: string
): MuseumPoi | null => {
const floorId = stringifyId(space.floorId) || fallbackFloorId
const position = normalizeSpaceCenter(space)
const hallId = stringifyId(space.id)
if (!hallId || !normalizedText(space.name) || !position) return null
return {
id: `hall-${hallId}`,
name: normalizedText(space.name),
floorId,
floorLabel: floorLabelForSgs(floorId, floors),
primaryCategory: hallCategory,
categories: [
hallCategory
],
positionGltf: position,
sourceObjectName: space.sourceNodeName || undefined,
sourceConfidence: 'backend-sgs-sdk-space-center',
navigationReadiness: '位置预览',
accessible: false,
kind: 'hall',
hallId,
hallName: normalizedText(space.name),
spaceId: hallId,
sourceSpaceId: hallId,
entrances: []
}
}
export const toMuseumHallPoisFromSgs = (
spaces: SgsSpacePayload[],
navigablePlaces: SgsNavigablePlacePayload[],
floors: SgsSdkFloorSummaryPayload[],
fallbackFloorId: string
): MuseumPoi[] => {
const hallSpaces = spaces.filter(isExhibitionHallSpace)
const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
const halls = new Map<string, MuseumPoi>()
hallPlaces.forEach((place, index) => {
const matchedSpace = findMatchedHallSpace(place, hallSpaces)
const entrance = createHallEntrance(place, floors, fallbackFloorId)
const entranceHasRouteAnchor = Boolean(entrance.positionGltf && entrance.routeNodeId)
const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined
const floorId = entrance.floorId || stringifyId(matchedSpace?.floorId) || fallbackFloorId
const hallId = stringifyId(matchedSpace?.id) || undefined
const poiId = createHallPoiId(matchedSpace, place, index)
const hallName = normalizedText(matchedSpace?.name) || placeHallName(place) || entrance.name
const existing = halls.get(poiId)
if (existing) {
existing.entrances = [
...(existing.entrances || []),
entrance
]
if (entranceHasRouteAnchor) {
existing.positionGltf = entrance.positionGltf
existing.floorId = floorId
existing.floorLabel = entrance.floorLabel
existing.sourceObjectName = entrance.sourceObjectName
existing.sourcePlaceId = entrance.sourcePlaceId
existing.sourceConfidence = 'backend-sgs-sdk-hall-entrance'
return
}
if (!existing.positionGltf && fallbackPosition) {
existing.positionGltf = fallbackPosition
existing.floorId = floorId
existing.floorLabel = entrance.floorLabel
existing.sourceObjectName = matchedSpace?.sourceNodeName || undefined
existing.sourcePlaceId = entrance.sourcePlaceId
existing.sourceConfidence = 'backend-sgs-sdk-space-center'
}
return
}
const positionGltf = entranceHasRouteAnchor ? entrance.positionGltf : fallbackPosition
if (!positionGltf) return
halls.set(poiId, {
id: poiId,
name: hallName,
floorId,
floorLabel: entrance.floorLabel,
primaryCategory: hallCategory,
categories: [
hallCategory,
hallEntranceCategory
],
positionGltf,
sourceObjectName: entranceHasRouteAnchor
? entrance.sourceObjectName
: matchedSpace?.sourceNodeName || undefined,
sourceConfidence: entranceHasRouteAnchor
? 'backend-sgs-sdk-hall-entrance'
: 'backend-sgs-sdk-space-center',
navigationReadiness: '位置预览',
accessible: false,
kind: 'hall',
hallId,
hallName,
spaceId: hallId,
sourcePlaceId: entrance.sourcePlaceId,
sourceSpaceId: hallId,
entrances: [entrance]
})
})
hallSpaces.forEach((space) => {
const hallId = stringifyId(space.id)
if (!hallId || halls.has(`hall-${hallId}`)) return
const fallbackPoi = createSpaceFallbackHallPoi(space, floors, fallbackFloorId)
if (fallbackPoi) {
halls.set(fallbackPoi.id, fallbackPoi)
}
})
return Array.from(halls.values())
.filter((poi) => poi.id && poi.name && poi.positionGltf)
}
export const createSgsHallPoiDiagnostics = (
spaces: SgsSpacePayload[],
navigablePlaces: SgsNavigablePlacePayload[],
hallPois: MuseumPoi[]
): SgsHallPoiDiagnostics => {
const eligibleSpaceCount = spaces.filter(isExhibitionHallSpace).length
const hallPlaceCount = navigablePlaces.filter(isExhibitionHallNavigablePlace).length
const hallPoiWithPositionCount = hallPois.filter((poi) => Boolean(poi.positionGltf)).length
return {
spaceCount: spaces.length,
eligibleSpaceCount,
navigablePlaceCount: navigablePlaces.length,
hallPlaceCount,
hallPoiCount: hallPois.length,
hallPoiWithPositionCount,
skippedSpaceCount: Math.max(eligibleSpaceCount - hallPois.length, 0),
skippedPlaceCount: Math.max(hallPlaceCount - hallPois.reduce((count, poi) => count + (poi.entrances?.length || 0), 0), 0)
}
}
export const toGuideFloorDetailFromSgs = (
floor: SgsSdkFloorSummaryPayload | SgsFloorDiagnosticsPayload
): GuideFloorDetail => ({

View File

@@ -72,6 +72,19 @@ export interface MuseumCategory {
iconType?: string
}
export type MuseumPoiKind = 'facility' | 'hall' | 'hall_entrance' | 'guide'
export interface MuseumPoiEntrance {
id: string
name: string
floorId: string
floorLabel: string
positionGltf?: [number, number, number]
sourceObjectName?: string
sourcePlaceId?: string
routeNodeId?: string
}
export interface MuseumPoi {
id: string
name: string
@@ -84,6 +97,13 @@ export interface MuseumPoi {
sourceConfidence?: string
navigationReadiness?: string
accessible: boolean
kind?: MuseumPoiKind
hallId?: string
hallName?: string
spaceId?: string
sourcePlaceId?: string
sourceSpaceId?: string
entrances?: MuseumPoiEntrance[]
}
export type GuideLocationResolutionStatus = 'exact' | 'hallFallback' | 'candidate' | 'unavailable'
@@ -149,6 +169,10 @@ export interface GuideLocationPreview {
primaryCategoryZh?: string
positionGltf?: [number, number, number]
sourceObjectName?: string
kind?: MuseumPoiKind
hallId?: string
hallName?: string
entrances?: MuseumPoiEntrance[]
}
export interface GuideStartLocation {