Files
frontend-miniapp/scripts/check-sgs-hall-poi-adapter.mjs

207 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
const exhibitionSpaceTypeWhitelist = new Set([
'exhibition_hall',
'theater',
'education_activity',
'ramp'
])
const exhibitionSpaceKeywords = [
'展厅',
'临展',
'展览',
'影院',
'球幕',
'巨幕',
'报告厅',
'活动室',
'科普',
'实验室',
'展览坡道',
'宇宙',
'地球',
'演化',
'恐龙',
'人类',
'生物',
'生态',
'家园'
]
const normalizedText = (value) => (value || '').trim()
const normalizedHallName = (value) => normalizedText(value).replace(/[\s()【】\[\]_-]/g, '')
const hasExhibitionKeyword = (value) => exhibitionSpaceKeywords.some((keyword) => normalizedText(value).includes(keyword))
const isEligiblePublicExhibitionSpace = (space) => {
const type = normalizedText(space.type).toLowerCase()
const name = normalizedText(space.name)
// Finding 2: Enforce whitelist first, reject unrelated types before keyword heuristics
if (!exhibitionSpaceTypeWhitelist.has(type)) return false
if (type === 'exhibition_hall') return true
if (type === 'theater') return hasExhibitionKeyword(name)
if (type === 'education_activity') return hasExhibitionKeyword(name)
if (type === 'ramp') return name.includes('展览坡道')
return false
}
const placeHallName = (place) => (
normalizedText(place.ownerName)
|| normalizedText(place.name)
.replace(/(?:主)?(?:出入口|入口|出口|门)\s*[A-Za-z0-9一二三四五六七八九十号-]*$/u, '')
.trim()
|| normalizedText(place.name)
)
const isExhibitionHallNavigablePlace = (place) => {
const searchableText = [
place.name,
place.ownerName,
place.category,
place.type,
place.typeCode,
place.typeName
]
.map(normalizedText)
.filter(Boolean)
.join(' ')
return hasExhibitionKeyword(searchableText)
}
const findMatchedHallSpace = (place, spaces) => {
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 normalizePositionSource = (source) => {
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) => 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 normalizeSpaceCenter = (space) => normalizePositionSource(space.center || {})
const createHallPoiId = (space, place, fallbackIndex) => {
if (space?.id) return `hall-${space.id}`
const ownerName = normalizedHallName(placeHallName(place))
if (ownerName) return `hall-place-${ownerName}`
return `hall-place-${place.id || fallbackIndex + 1}`
}
const toHallPois = (spaces, navigablePlaces, fallbackFloorId) => {
const hallSpaces = spaces.filter(isEligiblePublicExhibitionSpace)
const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
const halls = new Map()
hallPlaces.forEach((place, index) => {
const matchedSpace = findMatchedHallSpace(place, hallSpaces)
const entrancePosition = normalizePlacePosition(place)
const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined
const position = entrancePosition || fallbackPosition
const poiId = createHallPoiId(matchedSpace, place, index)
if (!position) return
const existing = halls.get(poiId)
if (existing) {
existing.entrances.push({ name: place.name, positionGltf: entrancePosition })
return
}
halls.set(poiId, {
id: poiId,
name: matchedSpace?.name || placeHallName(place),
floorId: String(place.floorId || matchedSpace?.floorId || fallbackFloorId),
primaryCategory: 'exhibition_hall',
positionGltf: position,
sourceConfidence: entrancePosition ? 'backend-sgs-sdk-hall-entrance' : 'backend-sgs-sdk-space-center',
entrances: [{ name: place.name, positionGltf: entrancePosition }]
})
})
hallSpaces.forEach((space) => {
const poiId = `hall-${space.id}`
if (halls.has(poiId)) return
const position = normalizeSpaceCenter(space)
if (!position) return
halls.set(poiId, {
id: poiId,
name: space.name,
floorId: String(space.floorId || fallbackFloorId),
primaryCategory: 'exhibition_hall',
positionGltf: position,
sourceConfidence: 'backend-sgs-sdk-space-center',
entrances: []
})
})
return Array.from(halls.values())
}
const spaces = [
{ id: 1, name: '展厅1宇宙厅', type: 'exhibition_hall', floorId: 'B2', center: { x: 10, y: 0, z: 20 } },
{ id: 2, name: '巨幕影院', type: 'theater', floorId: 'L1', center: { x: 30, y: 0, z: 40 } },
{ id: 3, name: '博物馆之友活动室', type: 'education_activity', floorId: 'L4', center: { x: 50, y: 0, z: 60 } },
{ id: 4, name: '展览坡道', type: 'ramp', floorId: 'L1.5', center: { x: 70, y: 0, z: 80 } },
{ id: 5, name: '茶水间', type: 'service', floorId: 'L1', center: { x: 90, y: 0, z: 100 } },
// Finding 3: unmatched exhibition space with center coordinates emits fallback POI
{ id: 6, name: '自然科学展厅', type: 'exhibition_hall', floorId: 'L2', center: { x: 110, y: 0, z: 120 } },
// Finding 4: space that will match when entrance has no coordinates
{ id: 7, name: '地球科学展厅', type: 'exhibition_hall', floorId: 'L3', center: { x: 130, y: 0, z: 140 } }
]
const places = [
{ id: 101, name: '展厅1宇宙厅 出入口 1', ownerName: '展厅1宇宙厅', floorId: 'B2', position: { x: 11, y: 0, z: 21 }, nodeId: 'n101' },
{ id: 102, name: '巨幕影院 出入口 1', ownerName: '巨幕影院', floorId: 'L1', position: { x: 31, y: 0, z: 41 }, nodeId: 'n102' },
// Finding 4: entrance with missing coordinates falls back to matched space.center
{ id: 104, name: '地球科学展厅 出入口', ownerName: '地球科学展厅', floorId: 'L3' }
]
const result = toHallPois(spaces, places, 'B2')
const byName = new Map(result.map((poi) => [poi.name, poi]))
// Original coverage
assert.equal(result.length, 6)
assert.deepEqual(byName.get('展厅1宇宙厅').positionGltf, [11, 0, 21])
assert.deepEqual(byName.get('巨幕影院').positionGltf, [31, 0, 41])
assert.deepEqual(byName.get('博物馆之友活动室').positionGltf, [50, 0, 60])
assert.deepEqual(byName.get('展览坡道').positionGltf, [70, 0, 80])
assert.equal(byName.has('茶水间'), false)
assert.equal(byName.get('博物馆之友活动室').sourceConfidence, 'backend-sgs-sdk-space-center')
// Finding 2: whitelist enforcement prevents unrelated types
assert.equal(byName.has('茶水间'), false, 'service type rejected by whitelist')
// Finding 3: unmatched exhibition space with center emits fallback POI
assert.equal(byName.has('自然科学展厅'), true, 'unmatched space-center POI emitted')
assert.deepEqual(byName.get('自然科学展厅').positionGltf, [110, 0, 120])
assert.equal(byName.get('自然科学展厅').sourceConfidence, 'backend-sgs-sdk-space-center')
assert.deepEqual(byName.get('自然科学展厅').entrances, [])
// Finding 4: entrance with missing coordinates falls back to matched space.center
assert.equal(byName.has('地球科学展厅'), true, 'entrance fallback to space.center')
assert.deepEqual(byName.get('地球科学展厅').positionGltf, [130, 0, 140])
assert.equal(byName.get('地球科学展厅').sourceConfidence, 'backend-sgs-sdk-space-center', 'Finding 5: sourceConfidence from space-center not entrance')
// Finding 4: entrance still recorded even with undefined positionGltf (no coordinates)
assert.equal(byName.get('地球科学展厅').entrances[0].name, '地球科学展厅 出入口')
assert.equal(byName.get('地球科学展厅').entrances[0].positionGltf, undefined)
console.log('SGS hall POI adapter smoke check passed')