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) 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, index) => { 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 } } ] 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' } ] const result = toHallPois(spaces, places, 'B2') const byName = new Map(result.map((poi) => [poi.name, poi])) assert.equal(result.length, 4) 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') console.log('SGS hall POI adapter smoke check passed')