@@ -15,6 +15,9 @@ import {
|
||||
isIndoorNavigableFloor,
|
||||
isPoiOnIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
warnPoiCollectionIssues
|
||||
} from '@/domain/poiCategories'
|
||||
import {
|
||||
formatNavFloorLabel,
|
||||
isStaticIndoorNavigableFloorId,
|
||||
@@ -127,6 +130,91 @@ const dedupePoisById = (pois: MuseumPoi[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getPoiSpaceIdentity = (poi: MuseumPoi) => poi.sourceSpaceId || poi.spaceId || ''
|
||||
|
||||
const mergeSearchablePois = (pois: MuseumPoi[], spacePoints: MuseumPoi[]) => {
|
||||
const hallPoisBySpaceId = new Map(
|
||||
pois
|
||||
.map((poi) => [getPoiSpaceIdentity(poi), poi] as const)
|
||||
.filter(([spaceId]) => Boolean(spaceId))
|
||||
)
|
||||
const spacePointsBySpaceId = new Map(
|
||||
spacePoints
|
||||
.map((poi) => [getPoiSpaceIdentity(poi), poi] as const)
|
||||
.filter(([spaceId]) => Boolean(spaceId))
|
||||
)
|
||||
|
||||
const filteredPois = pois.filter((poi) => {
|
||||
const spaceId = getPoiSpaceIdentity(poi)
|
||||
const matchingSpacePoint = spaceId ? spacePointsBySpaceId.get(spaceId) : null
|
||||
if (!matchingSpacePoint) return true
|
||||
|
||||
return matchingSpacePoint.primaryCategory.id === 'exhibition_hall'
|
||||
})
|
||||
const filteredSpacePoints = spacePoints.filter((poi) => {
|
||||
const spaceId = getPoiSpaceIdentity(poi)
|
||||
const matchingHallPoi = spaceId ? hallPoisBySpaceId.get(spaceId) : null
|
||||
if (!matchingHallPoi) return true
|
||||
|
||||
return poi.primaryCategory.id !== 'exhibition_hall'
|
||||
})
|
||||
|
||||
return dedupePoisById([
|
||||
...filteredPois,
|
||||
...filteredSpacePoints
|
||||
])
|
||||
}
|
||||
|
||||
const getMedianPoiY = (pois: MuseumPoi[]) => {
|
||||
const values = pois
|
||||
.map((poi) => poi.positionGltf?.[1])
|
||||
.filter((value): value is number => Number.isFinite(value))
|
||||
.sort((left, right) => left - right)
|
||||
|
||||
if (!values.length) return undefined
|
||||
const middle = Math.floor(values.length / 2)
|
||||
return values.length % 2 === 0
|
||||
? (values[middle - 1] + values[middle]) / 2
|
||||
: values[middle]
|
||||
}
|
||||
|
||||
type SgsPoiSourceName = 'pois' | 'businessPois' | 'spaces' | 'navigablePlaces'
|
||||
|
||||
interface SgsPoiSourceFailure {
|
||||
floorId: string
|
||||
source: SgsPoiSourceName
|
||||
message: string
|
||||
}
|
||||
|
||||
const poiDataLoadErrorMessage = '点位数据加载失败,请检查网络后重试'
|
||||
|
||||
const toErrorMessage = (reason: unknown) => (
|
||||
reason instanceof Error ? reason.message : String(reason || '未知错误')
|
||||
)
|
||||
|
||||
const collectSgsSourceResult = <T>(
|
||||
result: PromiseSettledResult<T>,
|
||||
floorId: string,
|
||||
source: SgsPoiSourceName,
|
||||
failures: SgsPoiSourceFailure[]
|
||||
) => {
|
||||
if (result.status === 'fulfilled') return result.value
|
||||
|
||||
failures.push({
|
||||
floorId,
|
||||
source,
|
||||
message: toErrorMessage(result.reason)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const warnSgsSourceFailures = (scope: string, failures: SgsPoiSourceFailure[]) => {
|
||||
if (!import.meta.env.DEV || !failures.length) return
|
||||
|
||||
// 开发期保留来源与楼层信息,便于区分真实空结果和接口失败。
|
||||
console.warn(`[SGS 点位数据] ${scope}`, failures)
|
||||
}
|
||||
|
||||
export interface GuideRepository {
|
||||
getAssetBaseUrl(): string
|
||||
getFloors(): Promise<MuseumFloor[]>
|
||||
@@ -171,9 +259,11 @@ export class StaticGuideRepository implements GuideRepository {
|
||||
if (this.poiCache) return this.poiCache
|
||||
|
||||
const pois = await this.provider.loadPoiIndex()
|
||||
this.poiCache = pois
|
||||
.map(toMuseumPoi)
|
||||
const adaptedPois = pois.map(toMuseumPoi)
|
||||
warnPoiCollectionIssues('static-nav-assets', adaptedPois)
|
||||
this.poiCache = dedupePoisById(adaptedPois
|
||||
.filter(isSearchableIndoorPoi)
|
||||
)
|
||||
|
||||
return this.poiCache
|
||||
}
|
||||
@@ -299,30 +389,55 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
||||
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
|
||||
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
||||
const failures: SgsPoiSourceFailure[] = []
|
||||
let successfulCoreSourceCount = 0
|
||||
const floorDataGroups = await Promise.all(
|
||||
indoorFloors.map(async (floor) => {
|
||||
const floorId = String(floor.floorId)
|
||||
const [pois, businessPois, spaces, navigablePlaces] = await Promise.all([
|
||||
this.provider.getFloorPois(floorId).catch(() => []),
|
||||
this.provider.getFloorBusinessPois(floorId).catch(() => []),
|
||||
this.provider.getFloorSpaces(floorId).catch(() => []),
|
||||
this.provider.getNavigablePlaces(floorId).catch(() => [])
|
||||
const [poisResult, businessPoisResult, spacesResult, navigablePlacesResult] = await Promise.allSettled([
|
||||
this.provider.getFloorPois(floorId),
|
||||
this.provider.getFloorBusinessPois(floorId),
|
||||
this.provider.getFloorSpaces(floorId),
|
||||
this.provider.getNavigablePlaces(floorId)
|
||||
])
|
||||
const pois = collectSgsSourceResult(poisResult, floorId, 'pois', failures) || []
|
||||
const businessPois = collectSgsSourceResult(businessPoisResult, floorId, 'businessPois', failures) || []
|
||||
const spaces = collectSgsSourceResult(spacesResult, floorId, 'spaces', failures) || []
|
||||
const navigablePlaces = collectSgsSourceResult(
|
||||
navigablePlacesResult,
|
||||
floorId,
|
||||
'navigablePlaces',
|
||||
failures
|
||||
) || []
|
||||
if (poisResult.status === 'fulfilled') successfulCoreSourceCount += 1
|
||||
if (spacesResult.status === 'fulfilled') successfulCoreSourceCount += 1
|
||||
const adaptedPois = [
|
||||
...pois,
|
||||
...businessPois
|
||||
].map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||
const fallbackY = getMedianPoiY(adaptedPois)
|
||||
|
||||
return {
|
||||
floorId,
|
||||
pois: [
|
||||
...pois,
|
||||
...businessPois
|
||||
],
|
||||
hallPois: toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId)
|
||||
pois: adaptedPois,
|
||||
hallPois: toMuseumHallPoisFromSgs(
|
||||
spaces,
|
||||
navigablePlaces,
|
||||
manifest.floors,
|
||||
floorId,
|
||||
{ fallbackY }
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
warnSgsSourceFailures('楼层点位聚合', failures)
|
||||
if (indoorFloors.length && successfulCoreSourceCount === 0) {
|
||||
throw new Error(poiDataLoadErrorMessage)
|
||||
}
|
||||
|
||||
const ordinaryPois = floorDataGroups
|
||||
.flatMap((group) => group.pois)
|
||||
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||
.filter((poi) => poi.id && poi.name)
|
||||
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||
|
||||
@@ -330,12 +445,18 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
.flatMap((group) => group.hallPois)
|
||||
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||
|
||||
this.poiCache = dedupePoisById([
|
||||
const adaptedPois = [
|
||||
...hallPois,
|
||||
...ordinaryPois
|
||||
])
|
||||
]
|
||||
warnPoiCollectionIssues('sgs-guide-pois-before-dedupe', adaptedPois)
|
||||
const result = dedupePoisById(adaptedPois)
|
||||
const hasCoreFailures = failures.some((failure) => (
|
||||
failure.source === 'pois' || failure.source === 'spaces'
|
||||
))
|
||||
if (!hasCoreFailures) this.poiCache = result
|
||||
|
||||
return this.poiCache
|
||||
return result
|
||||
}
|
||||
|
||||
async listSpacePoints() {
|
||||
@@ -345,48 +466,100 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
||||
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
|
||||
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
||||
const failures: SgsPoiSourceFailure[] = []
|
||||
let successfulSpaceSourceCount = 0
|
||||
|
||||
const floorDataGroups = await Promise.all(
|
||||
indoorFloors.map(async (floor) => {
|
||||
const floorId = String(floor.floorId)
|
||||
const spaces = await this.provider.getFloorSpaces(floorId).catch(() => [])
|
||||
const [spacesResult, floorPoisResult] = await Promise.allSettled([
|
||||
this.provider.getFloorSpaces(floorId),
|
||||
this.provider.getFloorPois(floorId)
|
||||
])
|
||||
const spaces = collectSgsSourceResult(spacesResult, floorId, 'spaces', failures) || []
|
||||
const floorPois = collectSgsSourceResult(floorPoisResult, floorId, 'pois', failures) || []
|
||||
if (spacesResult.status === 'fulfilled') successfulSpaceSourceCount += 1
|
||||
const fallbackY = getMedianPoiY(
|
||||
floorPois.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||
)
|
||||
|
||||
return {
|
||||
floorId,
|
||||
spaces
|
||||
spaces,
|
||||
fallbackY
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
warnSgsSourceFailures('空间点位聚合', failures)
|
||||
if (indoorFloors.length && successfulSpaceSourceCount === 0) {
|
||||
throw new Error(poiDataLoadErrorMessage)
|
||||
}
|
||||
|
||||
const spacePoints = floorDataGroups
|
||||
.flatMap((group) => group.spaces.map((space) => toMuseumSpacePointFromSgs(space, manifest.floors, group.floorId)))
|
||||
.flatMap((group) => group.spaces.map((space) => toMuseumSpacePointFromSgs(
|
||||
space,
|
||||
manifest.floors,
|
||||
group.floorId,
|
||||
{ fallbackY: group.fallbackY }
|
||||
)))
|
||||
.filter((poi): poi is MuseumPoi => Boolean(poi))
|
||||
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||
|
||||
this.spacePointCache = dedupePoisById(spacePoints)
|
||||
warnPoiCollectionIssues('sgs-space-points-before-dedupe', spacePoints)
|
||||
const result = dedupePoisById(spacePoints)
|
||||
const hasSpaceFailures = failures.some((failure) => failure.source === 'spaces')
|
||||
const floorsWithSpaces = new Set(
|
||||
floorDataGroups
|
||||
.filter((group) => group.spaces.length > 0)
|
||||
.map((group) => group.floorId)
|
||||
)
|
||||
const hasPoiHeightFallbackFailures = failures.some((failure) => (
|
||||
failure.source === 'pois' && floorsWithSpaces.has(failure.floorId)
|
||||
))
|
||||
if (!result.length && floorsWithSpaces.size && hasPoiHeightFallbackFailures) {
|
||||
throw new Error(poiDataLoadErrorMessage)
|
||||
}
|
||||
if (!hasSpaceFailures && !hasPoiHeightFallbackFailures) this.spacePointCache = result
|
||||
|
||||
return this.spacePointCache
|
||||
return result
|
||||
}
|
||||
|
||||
async getPoiById(id: string) {
|
||||
const pois = await this.listPois()
|
||||
const matchedPoi = pois.find((poi) => poi.id === id)
|
||||
const pois = await this.searchPois()
|
||||
return pois.find((poi) => poi.id === id || poi.spaceId === id || poi.sourceSpaceId === id)
|
||||
|| findPoiByNavIdNameFallback(pois, id)
|
||||
|
||||
if (matchedPoi) return matchedPoi
|
||||
|
||||
const spacePoints = await this.listSpacePoints()
|
||||
return spacePoints.find((poi) => poi.id === id || poi.spaceId === id || poi.sourceSpaceId === id)
|
||||
|| null
|
||||
}
|
||||
|
||||
async searchPois(keyword = '') {
|
||||
const pois = await this.listPois()
|
||||
const sourceFailures: unknown[] = []
|
||||
let pois: MuseumPoi[] = []
|
||||
let spacePoints: MuseumPoi[] = []
|
||||
|
||||
try {
|
||||
pois = await this.listPois()
|
||||
} catch (error) {
|
||||
sourceFailures.push(error)
|
||||
}
|
||||
|
||||
try {
|
||||
spacePoints = await this.listSpacePoints()
|
||||
} catch (error) {
|
||||
sourceFailures.push(error)
|
||||
}
|
||||
|
||||
const searchablePois = mergeSearchablePois(pois, spacePoints)
|
||||
if (sourceFailures.length && !searchablePois.length) {
|
||||
throw new Error(poiDataLoadErrorMessage)
|
||||
}
|
||||
if (import.meta.env.DEV && sourceFailures.length) {
|
||||
console.warn('[SGS 点位数据] 搜索使用部分可用数据', sourceFailures.map(toErrorMessage))
|
||||
}
|
||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||
|
||||
if (!normalizedKeyword) return pois
|
||||
if (!normalizedKeyword) return searchablePois
|
||||
|
||||
return pois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
|
||||
return searchablePois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
|
||||
}
|
||||
|
||||
async getLocationPreview(poiId: string) {
|
||||
@@ -425,7 +598,7 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
async checkDataIntegrity() {
|
||||
const [floors, pois, diagnostics] = await Promise.all([
|
||||
this.getFloors(),
|
||||
this.listPois(),
|
||||
this.searchPois(),
|
||||
this.getMapDiagnostics()
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user