diff --git a/scripts/check-sgs-hall-poi-adapter.mjs b/scripts/check-sgs-hall-poi-adapter.mjs new file mode 100644 index 0000000..7c8d395 --- /dev/null +++ b/scripts/check-sgs-hall-poi-adapter.mjs @@ -0,0 +1,179 @@ +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')