feat: expand SGS hall POI adaptation
This commit is contained in:
@@ -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 => ({
|
||||
|
||||
Reference in New Issue
Block a user