feat: wire guide data source and POI focus UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
122
src/repositories/GuideRepository.ts
Normal file
122
src/repositories/GuideRepository.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type {
|
||||
GuideLocationPreview,
|
||||
MuseumFloor,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
formatNavFloorLabel,
|
||||
navFloorIdFromLabel,
|
||||
toMuseumPoi
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
defaultStaticNavAssetsProvider,
|
||||
type StaticNavAssetsProvider
|
||||
} from '@/data/providers/staticNavAssetsProvider'
|
||||
|
||||
const searchableCategories = new Set([
|
||||
'touring_poi',
|
||||
'basic_service_facility',
|
||||
'transport_circulation',
|
||||
'accessibility_special_service',
|
||||
'operation_experience'
|
||||
])
|
||||
|
||||
const toSearchText = (poi: MuseumPoi) => [
|
||||
poi.name,
|
||||
poi.floorId,
|
||||
poi.floorLabel,
|
||||
poi.primaryCategory.label,
|
||||
poi.primaryCategory.iconType,
|
||||
...poi.categories.flatMap((category) => [
|
||||
category.label,
|
||||
category.id,
|
||||
category.iconType
|
||||
])
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
|
||||
const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
primaryCategoryZh: poi.primaryCategory.label,
|
||||
positionGltf: poi.positionGltf
|
||||
})
|
||||
|
||||
export interface GuideRepository {
|
||||
getAssetBaseUrl(): string
|
||||
getFloors(): Promise<MuseumFloor[]>
|
||||
normalizeFloorId(labelOrId: string): string
|
||||
listPois(): Promise<MuseumPoi[]>
|
||||
getPoiById(id: string): Promise<MuseumPoi | null>
|
||||
searchPois(keyword?: string): Promise<MuseumPoi[]>
|
||||
getLocationPreview(poiId: string): Promise<GuideLocationPreview | null>
|
||||
getRouteReadiness(): typeof NAV_ROUTE_READINESS
|
||||
}
|
||||
|
||||
export class StaticGuideRepository implements GuideRepository {
|
||||
private poiCache: MuseumPoi[] | null = null
|
||||
|
||||
constructor(private readonly provider: StaticNavAssetsProvider = defaultStaticNavAssetsProvider) {}
|
||||
|
||||
getAssetBaseUrl() {
|
||||
return this.provider.baseUrl
|
||||
}
|
||||
|
||||
async getFloors() {
|
||||
const floorIndex = await this.provider.loadFloorIndex()
|
||||
return [...floorIndex.floors]
|
||||
.sort((a, b) => b.order - a.order)
|
||||
.map((floor) => ({
|
||||
id: floor.floorId,
|
||||
label: formatNavFloorLabel(floor.floorId),
|
||||
order: floor.order
|
||||
}))
|
||||
}
|
||||
|
||||
normalizeFloorId(labelOrId: string) {
|
||||
return navFloorIdFromLabel(labelOrId)
|
||||
}
|
||||
|
||||
async listPois() {
|
||||
if (this.poiCache) return this.poiCache
|
||||
|
||||
const pois = await this.provider.loadPoiIndex()
|
||||
this.poiCache = pois
|
||||
.filter((poi) => searchableCategories.has(poi.primaryCategory))
|
||||
.map(toMuseumPoi)
|
||||
|
||||
return this.poiCache
|
||||
}
|
||||
|
||||
async getPoiById(id: string) {
|
||||
const pois = await this.listPois()
|
||||
return pois.find((poi) => poi.id === id) || null
|
||||
}
|
||||
|
||||
async searchPois(keyword = '') {
|
||||
const pois = await this.listPois()
|
||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||
|
||||
if (!normalizedKeyword) return pois
|
||||
|
||||
return pois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
|
||||
}
|
||||
|
||||
async getLocationPreview(poiId: string) {
|
||||
const poi = await this.getPoiById(poiId)
|
||||
return poi ? toLocationPreview(poi) : null
|
||||
}
|
||||
|
||||
getRouteReadiness() {
|
||||
return NAV_ROUTE_READINESS
|
||||
}
|
||||
}
|
||||
|
||||
export const guideRepository = new StaticGuideRepository()
|
||||
Reference in New Issue
Block a user