同步 sgs-frontend-mobile 源码
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-27 11:26:01 +08:00
parent 575dca430f
commit 9ea1bfab71
181 changed files with 24395 additions and 5212 deletions

View File

@@ -78,7 +78,6 @@ export interface BackendHall {
stopCount?: number | null
linkedExhibitCount?: number | null
audioReadyStopCount?: number | null
audioOptionCount?: number | null
hasAudio?: boolean | null
audioStatus?: string | null
supportedLanguages?: string[] | null
@@ -159,7 +158,6 @@ export interface BackendCatalogStopItem {
hasAudio?: boolean | null
audioStatus?: string | null
supportedLanguages?: string[] | null
audioOptionCount?: number | null
hasTextRecord?: boolean | null
playTargetType?: string | null
playTargetId?: string | number | null
@@ -289,7 +287,6 @@ export const toCatalogHall = (
stopCount: normalizeNumber(source.stopCount),
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
audioReadyStopCount: normalizeNumber(source.audioReadyStopCount),
audioOptionCount: normalizeNumber(source.audioOptionCount),
hasAudio: source.hasAudio === true,
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
@@ -362,7 +359,6 @@ export const toCatalogGuideStop = (
hasAudio: source.hasAudio === true,
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
audioOptionCount: normalizeNumber(source.audioOptionCount),
hasTextRecord: source.hasTextRecord === true,
poiId: stringifyId(source.poiId) || undefined,
mapX: normalizeNumber(source.mapX),
@@ -589,7 +585,7 @@ export const toBackendHall = (
name: firstText(first?.hallName, fallback?.name, '展厅'),
floorId: firstText(first?.floorId, fallback?.floorId) || undefined,
floorLabel: firstText(first?.floorLabel, fallback?.floorLabel, '楼层待补充'),
description: fallback?.description || '该展厅讲解内容来自后端展品接口。',
description: fallback?.description || '该免费讲解内容来自后端展品接口。',
image: fallback?.image || HALL_PLACEHOLDER_IMAGE,
exhibitCount: exhibits.length,
area: fallback?.area,

View File

@@ -1,12 +1,13 @@
import type {
AudioPlayTargetType
AudioPlayTargetType,
GuideAudioGender,
MuseumGuideAudioOption
} from '@/domain/museum'
import {
normalizeSameOriginPublicUrl
} from '@/utils/publicUrl'
export type GuideAudioLanguage = 'zh-CN' | 'yue-HK' | 'en-US'
export type GuideAudioVoiceGender = 'female' | 'male'
export interface BackendGuideStopLinkedExhibit {
id?: string | number | null
@@ -19,6 +20,20 @@ export interface BackendGuideStopLinkedExhibit {
sortOrder?: number | string | null
}
export interface BackendGuideAudioOption {
channelCode?: string | null
displayName?: string | null
version?: string | null
languageCode?: string | null
languageName?: string | null
gender?: string | null
playUrl?: string | null
duration?: number | string | null
format?: string | null
isDefault?: boolean | null
sortOrder?: number | string | null
}
export interface BackendGuideStopInfo {
available?: boolean
targetType?: string | null
@@ -43,45 +58,13 @@ export interface BackendGuideStopInfo {
hasText?: boolean
supportedLanguages?: string[] | null
audioStatus?: string | null
audioOptions?: BackendGuideStopAudioOption[] | null
languageVariants?: BackendGuideStopLanguageVariant[] | null
audioOptions?: BackendGuideAudioOption[] | null
audioOptionCount?: number | string | null
reason?: string | null
linkedExhibitCount?: number | string | null
isSharedStop?: boolean | null
}
export interface BackendGuideStopAudioOption {
channelCode?: string | null
displayName?: string | null
languageCode?: string | null
languageName?: string | null
gender?: string | null
playUrl?: string | null
duration?: number | string | null
format?: string | null
isDefault?: boolean | null
sortOrder?: number | string | null
}
export interface BackendGuideStopLanguageVariant {
lang?: string | null
enabled?: boolean | null
playable?: boolean | null
audioStatus?: string | null
playUrl?: string | null
duration?: number | string | null
format?: string | null
audioId?: string | number | null
narrationTier?: 'STANDARD' | 'EXTENDED' | string | null
hasText?: boolean | null
textAvailable?: boolean | null
text?: string | null
textLength?: number | string | null
textHash?: string | null
fallback?: boolean | null
reason?: string | null
}
export interface BackendAudioPlayInfo {
playable?: boolean
targetType?: string | null
@@ -125,38 +108,6 @@ export interface GuideStopLinkedExhibit {
sortOrder?: number
}
export interface GuideStopLanguageVariant {
lang: GuideAudioLanguage
enabled: boolean
playable: boolean
audioStatus: 'READY' | 'MISSING' | string
playUrl?: string
duration?: number
format?: string
audioId?: string
narrationTier?: 'STANDARD' | 'EXTENDED'
hasText: boolean
textAvailable: boolean
text?: string
textLength?: number
textHash?: string
fallback: boolean
reason?: string
}
export interface GuideStopAudioOption {
channelCode: string
displayName: string
languageCode: GuideAudioLanguage
languageName?: string
gender: GuideAudioVoiceGender
playUrl: string
duration?: number
format?: string
isDefault: boolean
sortOrder?: number
}
export interface GuideStopInfo {
available: boolean
targetType: AudioPlayTargetType
@@ -179,9 +130,9 @@ export interface GuideStopInfo {
hasAudio: boolean
hasText: boolean
supportedLanguages: GuideAudioLanguage[]
audioOptions: GuideStopAudioOption[]
languageVariants: Record<GuideAudioLanguage, GuideStopLanguageVariant>
audioStatus: 'READY' | 'MISSING' | string
audioOptions: MuseumGuideAudioOption[]
audioOptionCount?: number
reason?: string
linkedExhibitCount?: number
isSharedStop?: boolean
@@ -246,102 +197,47 @@ export const normalizeGuideAudioLanguage = (
return 'zh-CN'
}
const isSupportedGuideAudioLanguage = (value: string | null | undefined) => (
['zh', 'zh-cn', 'en', 'en-us', 'yue', 'yue-cn', 'yue-hk'].includes(value?.trim().toLowerCase() || '')
)
const normalizeSupportedLanguages = (languages: string[] | null | undefined): GuideAudioLanguage[] => (
Array.from(new Set((languages || [])
.filter(isSupportedGuideAudioLanguage)
.map(normalizeGuideAudioLanguage))) as GuideAudioLanguage[]
.map((language) => {
const normalized = language?.trim().toLowerCase()
if (!['zh', 'zh-cn', 'en', 'en-us', 'yue', 'yue-cn', 'yue-hk'].includes(normalized)) {
return null
}
return normalizeGuideAudioLanguage(language)
})
.filter(Boolean))) as GuideAudioLanguage[]
)
const normalizeLanguageVariants = (
variants: BackendGuideStopLanguageVariant[] | null | undefined
): Record<GuideAudioLanguage, GuideStopLanguageVariant> => {
const normalizedVariants = {} as Record<GuideAudioLanguage, GuideStopLanguageVariant>
;(variants || []).forEach((variant) => {
if (!isSupportedGuideAudioLanguage(variant.lang)) return
const lang = normalizeGuideAudioLanguage(variant.lang)
const playUrl = normalizeSameOriginPublicUrl(variant.playUrl) || undefined
const playable = variant.playable === true && Boolean(playUrl)
const text = variant.text?.trim() || undefined
const textAvailable = variant.textAvailable === true || Boolean(text)
normalizedVariants[lang] = {
lang,
enabled: variant.enabled === true,
playable,
audioStatus: variant.audioStatus || (playable ? 'READY' : 'MISSING'),
playUrl,
duration: normalizeNumber(variant.duration),
format: variant.format?.trim() || undefined,
audioId: stringifyId(variant.audioId) || undefined,
narrationTier: normalizeNarrationTier(variant.narrationTier),
hasText: variant.hasText === true || textAvailable,
textAvailable,
text,
textLength: normalizeNumber(variant.textLength),
textHash: variant.textHash?.trim() || undefined,
fallback: variant.fallback === true,
reason: variant.reason?.trim() || undefined
}
})
return normalizedVariants
}
const normalizeVoiceGender = (value: string | null | undefined): GuideAudioVoiceGender | undefined => {
const normalizeAudioGender = (value: string | null | undefined): GuideAudioGender | null => {
const normalized = value?.trim().toLowerCase()
return normalized === 'female' || normalized === 'male' ? normalized : undefined
return normalized === 'male' || normalized === 'female' ? normalized : null
}
const normalizeAudioOptions = (
options: BackendGuideStopAudioOption[] | null | undefined
): GuideStopAudioOption[] => (
(options || [])
.map<GuideStopAudioOption | null>((option) => {
if (!isSupportedGuideAudioLanguage(option.languageCode)) return null
const channelCode = option.channelCode?.trim()
const gender = normalizeVoiceGender(option.gender)
const playUrl = normalizeSameOriginPublicUrl(option.playUrl) || undefined
const normalizeAudioOptions = (items: BackendGuideAudioOption[] | null | undefined): MuseumGuideAudioOption[] => (
(items || [])
.map<MuseumGuideAudioOption | null>((item) => {
const channelCode = item.channelCode?.trim()
const languageCode = normalizeGuideAudioLanguage(item.languageCode)
const gender = normalizeAudioGender(item.gender)
const playUrl = normalizeSameOriginPublicUrl(item.playUrl)
if (!channelCode || !gender || !playUrl) return null
const languageCode = normalizeGuideAudioLanguage(option.languageCode)
const displayName = option.displayName?.trim()
|| `${languageCode === 'en-US' ? '英文' : languageCode === 'yue-HK' ? '粤语' : '普通话'}${gender === 'female' ? '女声' : '男声'}`
return {
channelCode,
displayName,
displayName: item.displayName?.trim() || undefined,
version: item.version?.trim() || undefined,
languageCode,
languageName: option.languageName?.trim() || undefined,
languageName: item.languageName?.trim() || undefined,
gender,
playUrl,
duration: normalizeNumber(option.duration),
format: option.format?.trim() || undefined,
isDefault: option.isDefault === true,
sortOrder: normalizeNumber(option.sortOrder)
duration: normalizeNumber(item.duration),
format: item.format?.trim() || undefined,
isDefault: item.isDefault === true,
sortOrder: normalizeNumber(item.sortOrder)
}
})
.filter(Boolean)
.sort((left, right) => (
(left!.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right!.sortOrder ?? Number.MAX_SAFE_INTEGER)
|| left!.channelCode.localeCompare(right!.channelCode)
)) as GuideStopAudioOption[]
)
const supportedLanguagesFromVariants = (variants: Record<GuideAudioLanguage, GuideStopLanguageVariant>) => (
(Object.values(variants) as GuideStopLanguageVariant[])
.filter((variant) => variant.enabled && (variant.playable || variant.textAvailable))
.map((variant) => variant.lang)
)
const supportedLanguagesFromAudioOptions = (audioOptions: GuideStopAudioOption[]) => (
Array.from(new Set(audioOptions.map((option) => option.languageCode)))
.filter(Boolean) as MuseumGuideAudioOption[]
)
const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => {
@@ -410,11 +306,11 @@ export const toGuideStopInfo = (
const coverImageUrl = canUseStopImages
? normalizeSameOriginPublicUrl(source.coverImageUrl) || undefined
: undefined
const languageVariants = normalizeLanguageVariants(source.languageVariants)
const audioOptions = normalizeAudioOptions(source.audioOptions)
const supportedLanguages = audioOptions.length
? supportedLanguagesFromAudioOptions(audioOptions)
: supportedLanguagesFromVariants(languageVariants)
const supportedLanguages = Array.from(new Set([
...normalizeSupportedLanguages(source.supportedLanguages),
...audioOptions.map((option) => option.languageCode as GuideAudioLanguage)
]))
return {
available: source.available === true,
@@ -437,12 +333,10 @@ export const toGuideStopInfo = (
playTargetId,
hasAudio: source.hasAudio === true,
hasText: source.hasText === true,
supportedLanguages: supportedLanguages.length
? supportedLanguages
: normalizeSupportedLanguages(source.supportedLanguages),
audioOptions,
languageVariants,
supportedLanguages,
audioStatus: source.audioStatus || 'MISSING',
audioOptions,
audioOptionCount: normalizeNumber(source.audioOptionCount),
reason: source.reason || undefined,
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
isSharedStop: source.isSharedStop === true

View File

@@ -10,7 +10,6 @@ import {
isIndoorNavigableFloor
} from '@/domain/guideFloor'
import {
isVisitorRestrictedPlaceName,
normalizePoiSemanticValue
} from '@/domain/poiCategories'
@@ -194,16 +193,6 @@ const isPoiAccessible = (poi: StaticNavPoiPayload) => (
|| poi.categories?.some((category) => category.topCategory === 'accessibility_special_service') === true
)
const resolveStaticPoiVisitorVisible = (
poi: StaticNavPoiPayload,
semanticType: string
) => (
typeof poi.visitorVisible === 'boolean'
? poi.visitorVisible
: semanticType !== 'service_space'
&& ![poi.name, poi.sourceObjectName].some(isVisitorRestrictedPlaceName)
)
export const toMuseumPoi = (poi: StaticNavPoiPayload): MuseumPoi => {
const categoryFallbackIconType = poi.categories?.[0]?.iconType
const iconType = getStaticPoiSemanticType(poi, poi.iconType || categoryFallbackIconType)
@@ -225,7 +214,6 @@ export const toMuseumPoi = (poi: StaticNavPoiPayload): MuseumPoi => {
sourceObjectName: poi.sourceObjectName,
sourceConfidence: poi.sourceConfidence,
navigationReadiness: poi.navigationReadiness,
visitorVisible: resolveStaticPoiVisitorVisible(poi, iconType),
accessible: isPoiAccessible(poi),
kind,
hallName: kind === 'hall' ? poi.name : undefined

View File

@@ -143,6 +143,7 @@ const toTarget = (
anchor: NavRouteAnchor,
poi?: StaticNavPoiPayload
): GuideRouteTarget => ({
routeTargetId: `${anchor.poiId}:${anchor.routeNodeId}`,
poiId: anchor.poiId,
name: poi?.name || anchor.name,
floorId: anchor.floorId,

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@ import type {
GuideRouteFloorSegment,
GuideRoutePoint,
GuideRouteResult,
GuideRouteTarget
GuideRouteTarget,
GuideRouteTransition
} from '@/domain/museum'
import type {
SgsRoutePathPointPayload,
@@ -13,7 +14,6 @@ import type {
import type {
SgsRouteResult as SgsSdkRuntimeRouteResult
} from '@/types/sgs-map-sdk'
import { toAppFloorId } from '@/services/sgs/SgsMapEventAdapter'
type SgsRoutePathNode = {
nodeId?: string | number
@@ -42,6 +42,87 @@ const stringifyRouteId = (value: unknown, fallback = '') => {
return String(value)
}
/**
* Route responses can mix the published database floor id with a floor code
* such as `L5` or `5F`. The renderer uses the published id as its model key,
* so route data must be resolved to that id at the adapter boundary.
*/
const canonicalFloorKey = (value: unknown) => {
const normalized = stringifyRouteId(value).trim().toUpperCase()
if (!normalized || /^\d{6,}$/.test(normalized)) return ''
if (normalized === 'EXTERIOR' || normalized.includes('室外') || normalized.includes('外观')) {
return 'exterior'
}
const basementMatch = normalized.match(/^(?:B|L-?|负)\s*(\d+(?:\.\d+)?)(?:层|F)?$/)
if (basementMatch && (normalized.startsWith('B') || normalized.startsWith('L-') || normalized.startsWith('负'))) {
return `l-${Number(basementMatch[1])}`
}
if (normalized === 'MF') return 'l1.5'
const floorMatch = normalized.match(/^(?:L|F)?\s*(\d+(?:\.\d+)?)(?:F|层)?$/)
if (floorMatch) return `l${Number(floorMatch[1])}`
const chineseFloorMatch = normalized.match(/^(?:负\s*)?(\d+(?:\.\d+)?)\s*层$/)
if (chineseFloorMatch) {
return normalized.startsWith('负')
? `l-${Number(chineseFloorMatch[1])}`
: `l${Number(chineseFloorMatch[1])}`
}
return ''
}
type RouteFloorIdResolver = (value: unknown, fallback?: string, hints?: unknown[]) => string
const createRouteFloorIdResolver = (
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget,
backendSegments: SgsRoutePlanResponsePayload['segments'] = []
): RouteFloorIdResolver => {
const aliases = new Map<string, string>()
const addAlias = (value: unknown, floorId: string) => {
const raw = stringifyRouteId(value).trim()
if (!raw || aliases.has(raw.toLowerCase())) return
aliases.set(raw.toLowerCase(), floorId)
const canonical = canonicalFloorKey(raw)
if (canonical && !aliases.has(canonical)) aliases.set(canonical, floorId)
}
const registerFloor = (floorId: unknown, ...tokens: unknown[]) => {
const resolvedFloorId = stringifyRouteId(floorId)
if (!resolvedFloorId) return
addAlias(resolvedFloorId, resolvedFloorId)
tokens.forEach((token) => addAlias(token, resolvedFloorId))
}
registerFloor(startTarget.floorId, startTarget.floorLabel)
registerFloor(endTarget.floorId, endTarget.floorLabel)
;(backendSegments || []).forEach((segment) => {
registerFloor(segment.floorId, segment.floorCode, segment.floorName)
registerFloor(segment.targetFloorId)
registerFloor(segment.fromFloorId)
})
return (value, fallback = '', hints = []) => {
const raw = stringifyRouteId(value)
if (raw && aliases.has(raw.toLowerCase())) return aliases.get(raw.toLowerCase()) || raw
const candidates = [raw, ...hints]
.map((candidate) => canonicalFloorKey(candidate))
.filter(Boolean)
for (const candidate of candidates) {
const resolved = aliases.get(candidate)
if (resolved) return resolved
}
return raw || fallback
}
}
const samePosition = (
a: [number, number, number],
b: [number, number, number]
@@ -53,10 +134,13 @@ const pointPosition = (point: SgsRoutePathNode): [number, number, number] => [
finiteNumber(point.z) ?? 0
]
const targetEndpoint = (target: GuideRouteTarget): GuideRouteEndpoint => ({
const targetEndpoint = (
target: GuideRouteTarget,
resolveFloorId: RouteFloorIdResolver
): GuideRouteEndpoint => ({
poiId: target.poiId,
name: target.name,
floorId: target.floorId,
floorId: resolveFloorId(target.floorId),
floorLabel: target.floorLabel,
routeNodeId: target.routeNodeId,
position: target.positionGltf || [0, 0, 0]
@@ -65,7 +149,8 @@ const targetEndpoint = (target: GuideRouteTarget): GuideRouteEndpoint => ({
const normalizePath = (
route: CompatibleSgsRouteResult,
startFloorId: string,
endFloorId: string
endFloorId: string,
resolveFloorId: RouteFloorIdResolver
): GuideRoutePoint[] => {
const pathPoints = route.pathPoints ?? route.path ?? []
@@ -76,15 +161,15 @@ const normalizePath = (
if (route.path?.length) {
return route.path.map((point, index) => ({
nodeId: String(point.nodeId || `sdk-route-point-${index}`),
floorId: toAppFloorId(point.floorId ?? startFloorId),
floorId: resolveFloorId(point.floorId, startFloorId),
position: pointPosition(point)
}))
}
const isCrossFloor = startFloorId !== endFloorId
const totalPoints = pathPoints.length
const normalizedStartFloorId = toAppFloorId(startFloorId)
const normalizedEndFloorId = toAppFloorId(endFloorId)
const normalizedStartFloorId = resolveFloorId(startFloorId, startFloorId)
const normalizedEndFloorId = resolveFloorId(endFloorId, endFloorId)
return pathPoints.map((point, index) => {
const nodeId = `sdk-pp-${index}`
@@ -113,10 +198,11 @@ const normalizePath = (
const createFloorSegments = (
points: GuideRoutePoint[],
start: GuideRouteTarget,
end: GuideRouteTarget
end: GuideRouteTarget,
resolveFloorId: RouteFloorIdResolver
): GuideRouteFloorSegment[] => {
const normalizedStartFloorId = toAppFloorId(start.floorId)
const normalizedEndFloorId = toAppFloorId(end.floorId)
const normalizedStartFloorId = resolveFloorId(start.floorId, start.floorId)
const normalizedEndFloorId = resolveFloorId(end.floorId, end.floorId)
const labels = new Map([
[normalizedStartFloorId, start.floorLabel],
@@ -125,15 +211,16 @@ const createFloorSegments = (
return points.reduce<GuideRouteFloorSegment[]>((segments, point) => {
const current = segments[segments.length - 1]
if (current && current.floorId === point.floorId) {
const pointFloorId = resolveFloorId(point.floorId, normalizedStartFloorId)
if (current && current.floorId === pointFloorId) {
current.points.push(point)
return segments
}
segments.push({
floorId: point.floorId,
floorLabel: labels.get(point.floorId) || point.floorId,
points: [point]
floorId: pointFloorId,
floorLabel: labels.get(pointFloorId) || pointFloorId,
points: [{ ...point, floorId: pointFloorId }]
})
return segments
}, [])
@@ -144,23 +231,25 @@ export const toGuideRouteResultFromSgs = (
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget
): GuideRouteResult => {
const start = targetEndpoint(startTarget)
const end = targetEndpoint(endTarget)
const resolveFloorId = createRouteFloorIdResolver(startTarget, endTarget)
const start = targetEndpoint(startTarget, resolveFloorId)
const end = targetEndpoint(endTarget, resolveFloorId)
const points = normalizePath(
route as CompatibleSgsRouteResult,
startTarget.floorId,
endTarget.floorId
start.floorId,
end.floorId,
resolveFloorId
)
const routePoints = points.length
? points
: [
{
nodeId: start.routeNodeId,
nodeId: start.routeNodeId || `${start.poiId}-start`,
floorId: start.floorId,
position: start.position
},
{
nodeId: end.routeNodeId,
nodeId: end.routeNodeId || `${end.poiId}-end`,
floorId: end.floorId,
position: end.position
}
@@ -173,27 +262,28 @@ export const toGuideRouteResultFromSgs = (
distanceMeters: Number(route.distance || 0),
nodeIds: routePoints.map((point) => point.nodeId),
points: routePoints,
floorSegments: createFloorSegments(routePoints, startTarget, endTarget),
floorSegments: createFloorSegments(routePoints, startTarget, endTarget, resolveFloorId),
connectorPoints: []
}
}
const floorLabelFor = (
floorId: string,
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget,
start: Pick<GuideRouteEndpoint, 'floorId' | 'floorLabel'>,
end: Pick<GuideRouteEndpoint, 'floorId' | 'floorLabel'>,
fallbackLabel?: string | null
) => {
if (fallbackLabel) return fallbackLabel
if (floorId === startTarget.floorId) return startTarget.floorLabel
if (floorId === endTarget.floorId) return endTarget.floorLabel
if (floorId === start.floorId) return start.floorLabel
if (floorId === end.floorId) return end.floorLabel
return floorId
}
const pointFromBackendNode = (
node: NonNullable<SgsRoutePlanResponsePayload['nodePaths']>[number],
index: number,
fallbackFloorId: string
fallbackFloorId: string,
resolveFloorId: RouteFloorIdResolver
): GuideRoutePoint | null => {
const x = finiteNumber(node.x)
const z = finiteNumber(node.y)
@@ -201,7 +291,7 @@ const pointFromBackendNode = (
return {
nodeId: stringifyRouteId(node.id, `sgs-route-node-${index}`),
floorId: stringifyRouteId(node.floorId, fallbackFloorId),
floorId: resolveFloorId(node.floorId, fallbackFloorId),
position: [
x,
finiteNumber(node.z) ?? 0,
@@ -304,23 +394,63 @@ const appendIfDifferent = (
const createPlanFloorSegments = (
points: GuideRoutePoint[],
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget
start: GuideRouteEndpoint,
end: GuideRouteEndpoint,
resolveFloorId: RouteFloorIdResolver
): GuideRouteFloorSegment[] => points.reduce<GuideRouteFloorSegment[]>((segments, point) => {
const floorId = resolveFloorId(point.floorId, start.floorId)
const current = segments[segments.length - 1]
if (current && current.floorId === point.floorId) {
current.points.push(point)
if (current && current.floorId === floorId) {
current.points.push({ ...point, floorId })
return segments
}
segments.push({
floorId: point.floorId,
floorLabel: floorLabelFor(point.floorId, startTarget, endTarget),
points: [point]
floorId,
floorLabel: floorLabelFor(floorId, start, end),
points: [{ ...point, floorId }]
})
return segments
}, [])
const createRouteTransitions = (
floorSegments: GuideRouteFloorSegment[],
backendSegments: SgsRoutePlanResponsePayload['segments'],
resolveFloorId: RouteFloorIdResolver
): GuideRouteTransition[] => {
const transferSegments = (backendSegments || []).filter((segment) => !isWalkSegment(segment))
const transitions: GuideRouteTransition[] = []
for (let index = 1; index < floorSegments.length; index += 1) {
const fromSegment = floorSegments[index - 1]
const toSegment = floorSegments[index]
const fromPoint = fromSegment.points[fromSegment.points.length - 1]
const toPoint = toSegment.points[0]
if (!fromPoint || !toPoint || fromSegment.floorId === toSegment.floorId) continue
const transfer = transferSegments[transitions.length]
const explicitFromFloorId = resolveFloorId(transfer?.fromFloorId, fromSegment.floorId)
const explicitToFloorId = resolveFloorId(transfer?.targetFloorId, toSegment.floorId)
// Prefer the formal transfer contract. The adjacent WALK segments are
// retained only as a fallback for route responses generated before it.
if (explicitFromFloorId !== fromSegment.floorId) continue
if (explicitToFloorId !== toSegment.floorId) continue
transitions.push({
id: `transition-${fromPoint.nodeId}-${toPoint.nodeId}`,
fromFloorId: explicitFromFloorId,
toFloorId: explicitToFloorId,
fromPosition: fromPoint.position,
toPosition: toPoint.position,
transferType: stringifyRouteId(transfer?.transferType || transfer?.segmentType || transfer?.type),
connectorName: transfer?.connectorName || transfer?.startNodeName || transfer?.endNodeName || undefined
})
}
return transitions
}
const isWalkSegment = (
segment: NonNullable<SgsRoutePlanResponsePayload['segments']>[number]
) => {
@@ -334,12 +464,17 @@ const createSegmentFloorSegments = (
end: GuideRouteEndpoint,
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget,
nodePoints: GuideRoutePoint[]
nodePoints: GuideRoutePoint[],
resolveFloorId: RouteFloorIdResolver
): GuideRouteFloorSegment[] => {
const segments = (route.segments || [])
.filter(isWalkSegment)
.map<GuideRouteFloorSegment | null>((segment, segmentIndex) => {
const floorId = stringifyRouteId(segment.floorId, start.floorId)
const floorId = resolveFloorId(
segment.floorId,
start.floorId,
[segment.floorCode, segment.floorName]
)
const nodeIdPrefix = `${floorId}-segment-${segmentIndex}`
const nodePathIds = new Set(
(segment.nodePathIds || [])
@@ -353,7 +488,7 @@ const createSegmentFloorSegments = (
const fallbackPoints = geoPoints.length
? geoPoints
: nodePoints.filter((point) => (
point.floorId === floorId
resolveFloorId(point.floorId, start.floorId) === floorId
&& (!nodePathIds.size || nodePathIds.has(point.nodeId))
))
@@ -361,7 +496,7 @@ const createSegmentFloorSegments = (
return {
floorId,
floorLabel: floorLabelFor(floorId, startTarget, endTarget, segment.floorName),
floorLabel: floorLabelFor(floorId, start, end, segment.floorName),
points: fallbackPoints.map((point, index) => ({
...point,
nodeId: point.nodeId || `${nodeIdPrefix}-${index}`
@@ -397,12 +532,13 @@ export const toGuideRouteResultFromSgsPlan = (
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget
): GuideRouteResult => {
const start = targetEndpoint(startTarget)
const end = targetEndpoint(endTarget)
const resolveFloorId = createRouteFloorIdResolver(startTarget, endTarget, route.segments)
const start = targetEndpoint(startTarget, resolveFloorId)
const end = targetEndpoint(endTarget, resolveFloorId)
const startPoint = routePointFromEndpoint(start, 'start')
const endPoint = routePointFromEndpoint(end, 'end')
const nodePoints = (route.nodePaths || [])
.map((node, index) => pointFromBackendNode(node, index, start.floorId))
.map((node, index) => pointFromBackendNode(node, index, start.floorId, resolveFloorId))
.filter((point): point is GuideRoutePoint => Boolean(point))
const segmentFloorSegments = createSegmentFloorSegments(
route,
@@ -410,7 +546,8 @@ export const toGuideRouteResultFromSgsPlan = (
end,
startTarget,
endTarget,
nodePoints
nodePoints,
resolveFloorId
)
const routeGeoPoints = pointsFromGeoJson(route.pathGeoJson, start.floorId, 'sgs-route')
const fallbackPoints = nodePoints.length
@@ -420,8 +557,9 @@ export const toGuideRouteResultFromSgsPlan = (
: [startPoint, endPoint]
const floorSegments = segmentFloorSegments.length
? segmentFloorSegments
: createPlanFloorSegments(fallbackPoints, startTarget, endTarget)
: createPlanFloorSegments(fallbackPoints, start, end, resolveFloorId)
const routePoints = floorSegments.flatMap((segment) => segment.points)
const transitions = createRouteTransitions(floorSegments, route.segments, resolveFloorId)
return {
id: `sgs-api-route-${start.poiId}-${end.poiId}`,
@@ -431,21 +569,22 @@ export const toGuideRouteResultFromSgsPlan = (
nodeIds: routePoints.map((point) => point.nodeId),
points: routePoints,
floorSegments,
connectorPoints: extractFloorConnectorPoints(routePoints, startTarget, endTarget)
connectorPoints: extractFloorConnectorPoints(routePoints, start, end),
transitions
}
}
const extractFloorConnectorPoints = (
points: GuideRoutePoint[],
startTarget: GuideRouteTarget,
endTarget: GuideRouteTarget
start: GuideRouteEndpoint,
end: GuideRouteEndpoint
): GuideRouteConnectorPoint[] => {
if (points.length < 2) return []
const connectors: GuideRouteConnectorPoint[] = []
const labels = new Map<string, string>([
[startTarget.floorId, startTarget.floorLabel],
[endTarget.floorId, endTarget.floorLabel]
[start.floorId, start.floorLabel],
[end.floorId, end.floorLabel]
])
for (let i = 0; i < points.length - 1; i++) {

View File

@@ -21,6 +21,10 @@ import {
type BackendCatalogOutlineItem,
type BackendCatalogStopItem
} from '@/data/adapters/backendExplainDataAdapter'
import {
readPersistentJsonCache,
writePersistentJsonCache
} from '@/utils/persistentJsonCache'
interface CommonResult<T> {
code: number
@@ -78,6 +82,7 @@ const catalogLang = () => dataSourceConfig.audioLanguage
const cacheKey = (...parts: string[]) => [catalogLang(), ...parts].join(':')
const CATALOG_CACHE_TTL_MS = 60_000
const CATALOG_PERSISTENT_CACHE_TTL_MS = 5 * 60_000
const CATALOG_CACHE_MAX_ENTRIES = 80
interface TimedCacheEntry<T> {
@@ -148,6 +153,24 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
}
}
private persistentCacheKey(key: string) {
return [
'sgs-mobile',
'explain-catalog',
'v1',
dataSourceConfig.apiBaseUrl,
key
].map((part) => encodeURIComponent(part)).join(':')
}
private readPersistent<T>(key: string, allowExpired = false) {
return readPersistentJsonCache<T>(this.persistentCacheKey(key), allowExpired)
}
private writePersistent<T>(key: string, value: T) {
writePersistentJsonCache(this.persistentCacheKey(key), value, CATALOG_PERSISTENT_CACHE_TTL_MS)
}
private async safeFallbackHalls() {
if (!this.isStaticFallbackEnabled()) return []
@@ -177,6 +200,12 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const cached = this.getCached(this.hallListCache, key)
if (cached) return cached
const persistent = this.readPersistent<MuseumHall[]>(key)
if (persistent) {
this.setCached(this.hallListCache, key, persistent)
return persistent
}
const inflight = this.hallListInflight.get(key)
if (inflight) return inflight
@@ -196,8 +225,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
.filter((hall) => hall.id)
this.setCached(this.hallListCache, key, halls)
this.writePersistent(key, halls)
return halls
})()
})().catch((error) => {
const stale = this.readPersistent<MuseumHall[]>(key, true)
if (stale) return stale
throw error
})
this.hallListInflight.set(key, promise)
try {
@@ -215,6 +249,12 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const cached = this.getCached(this.outlineCache, key)
if (cached) return cached
const persistent = this.readPersistent<BackendCatalogOutlineItem[]>(key)
if (persistent) {
this.setCached(this.outlineCache, key, persistent)
return persistent
}
const inflight = this.outlineInflight.get(key)
if (inflight) return inflight
@@ -227,8 +267,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const response = await requestJson<CommonResult<BackendCatalogOutlineItem[]>>(url)
const outlines = requireArrayData(response, '讲解单元目录加载失败')
this.setCached(this.outlineCache, key, outlines)
this.writePersistent(key, outlines)
return outlines
})()
})().catch((error) => {
const stale = this.readPersistent<BackendCatalogOutlineItem[]>(key, true)
if (stale) return stale
throw error
})
this.outlineInflight.set(key, promise)
try {
@@ -248,6 +293,12 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const cached = this.getCached(this.guideStopPageCache, key)
if (cached) return cached
const persistent = this.readPersistent<ExplainGuideStopPage>(key)
if (persistent) {
this.setCached(this.guideStopPageCache, key, persistent)
return persistent
}
const inflight = this.guideStopPageInflight.get(key)
if (inflight) return inflight
@@ -268,8 +319,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
hasMore: normalizedPageNo * normalizedPageSize < data.total
}
this.setCached(this.guideStopPageCache, key, page)
this.writePersistent(key, page)
return page
})()
})().catch((error) => {
const stale = this.readPersistent<ExplainGuideStopPage>(key, true)
if (stale) return stale
throw error
})
this.guideStopPageInflight.set(key, promise)
try {
@@ -286,6 +342,12 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const cached = this.getCached(this.guideStopCache, key)
if (cached) return cached
const persistent = this.readPersistent<ExplainGuideStop[]>(key)
if (persistent) {
this.setCached(this.guideStopCache, key, persistent)
return persistent
}
const inflight = this.guideStopInflight.get(key)
if (inflight) return inflight
@@ -303,8 +365,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
if (!page.hasMore || stops.length >= page.total) break
}
this.setCached(this.guideStopCache, key, stops)
this.writePersistent(key, stops)
return stops
})()
})().catch((error) => {
const stale = this.readPersistent<ExplainGuideStop[]>(key, true)
if (stale) return stale
throw error
})
this.guideStopInflight.set(key, promise)
try {
@@ -329,6 +396,12 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
const cached = this.getCached(this.guideStopCache, key)
if (cached) return cached
const persistent = this.readPersistent<ExplainGuideStop[]>(key)
if (persistent) {
this.setCached(this.guideStopCache, key, persistent)
return persistent
}
const inflight = this.guideStopInflight.get(key)
if (inflight) return inflight
@@ -353,8 +426,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
.filter(Boolean) as ExplainGuideStop[]
this.setCached(this.guideStopCache, key, stops)
this.writePersistent(key, stops)
return stops
})()
})().catch((error) => {
const stale = this.readPersistent<ExplainGuideStop[]>(key, true)
if (stale) return stale
throw error
})
this.guideStopInflight.set(key, promise)
try {

View File

@@ -1,6 +1,13 @@
import {
dataSourceConfig
} from '@/config/dataSource'
import {
startGuidePerformance
} from '@/services/performance/guidePerformance'
import {
readPersistentJsonCache,
writePersistentJsonCache
} from '@/utils/persistentJsonCache'
export type SgsDiagnosticsStatusPayload = 'OK' | 'WARN' | 'ERROR'
@@ -10,6 +17,11 @@ export interface SgsSdkFloorSummaryPayload {
floorName?: string | null
sortOrder?: number | null
modelSizeBytes?: number | null
/** Manifest 首屏模型元数据,避免仅为模型地址读取整份楼层 Bundle。 */
modelUrl?: string | null
fallbackModelUrl?: string | null
compressionType?: 'draco' | 'none' | string | null
modelVersion?: string | null
poiCount?: number | null
spaceCount?: number | null
}
@@ -23,9 +35,23 @@ export interface SgsSdkManifestPayload {
updatedAt?: string | null
coordinateSystem?: string | null
floors: SgsSdkFloorSummaryPayload[]
routeAssets?: SgsRouteAssetPayload[]
capabilities?: Record<string, boolean | undefined>
}
export interface SgsRouteAssetPayload {
id?: string | number | null
floorId?: string | number | null
floorCode?: string | null
assetRole?: string | null
modelUrl?: string | null
sourceFileName?: string | null
sourceNodeName?: string | null
modelVersion?: string | null
coverageFloorCodes?: string[] | null
sortOrder?: number | null
}
export type SgsPoiGroupPayload = 'SERVICE' | 'BUSINESS' | 'OTHER'
export type SgsBusinessPoiTypePayload =
@@ -60,13 +86,16 @@ export interface SgsPoiPayload {
typeName?: string | null
floorCode?: string | null
floorId?: string | number | null
meshCenter?: SgsPositionPayload | null
position?: SgsPositionPayload | null
labelPosition?: SgsPositionPayload | null
x?: number | null
y?: number | null
z?: number | null
status?: string | null
visitorVisible?: boolean | null
anchorNodeName?: string | null
sourceNodeName?: string | null
extParams?: string | Record<string, unknown> | null
description?: string | null
iconUrl?: string | null
poiGroup?: SgsPoiGroupPayload | string | null
@@ -82,13 +111,19 @@ export interface SgsPoiPayload {
export interface SgsSpacePayload {
id: string | number
name?: string | null
displayName?: string | null
type?: string | null
typeName?: string | null
floorId?: string | number | null
floorCode?: string | null
boundaryWkt?: string | null
labelPosition?: SgsPositionPayload | null
meshCenter?: SgsPositionPayload | null
position?: SgsPositionPayload | null
center?: SgsPositionPayload | null
centerPoint?: SgsPositionPayload | null
sourceNodeName?: string | null
status?: string | null
visitorVisible?: boolean | null
colorHex?: string | null
}
@@ -107,6 +142,9 @@ export interface SgsNavigablePlacePayload {
y?: number | null
z?: number | null
nodeId?: string | number | null
anchorId?: string | null
anchorType?: string | null
sourceId?: string | number | null
ownerName?: string | null
}
@@ -182,6 +220,8 @@ export interface SgsFloorBundlePayload {
model?: SgsModelInfoPayload | null
pois?: SgsPoiPayload[]
spaces?: SgsSpacePayload[]
businessPois?: SgsPoiPayload[]
navigablePlaces?: SgsNavigablePlacePayload[]
guideStops?: SgsGuideStopPayload[]
routeSummary?: {
hasRouteNetwork?: boolean
@@ -213,6 +253,8 @@ export interface SgsRoutePlanRequestPayload {
endY: number
endNodeId?: number | string | null
wheelchair: 0 | 1
/** Public visitor navigation avoids stairs when an elevator or escalator route exists. */
verticalTransferPolicy?: 'PREFER_ELEVATOR_ESCALATOR'
}
export interface SgsRouteStepNodePayload {
@@ -233,6 +275,7 @@ export interface SgsRoutePathPointPayload {
export interface SgsRouteSegmentPayload {
floorId?: string | number | null
fromFloorId?: string | number | null
floorCode?: string | null
floorName?: string | null
startNodeId?: string | number | null
@@ -244,6 +287,8 @@ export interface SgsRouteSegmentPayload {
transferType?: string | null
distance?: number | null
duration?: number | null
connectorName?: string | null
targetFloorId?: string | number | null
pathGeoJson?: string | null
pathPoints?: SgsRoutePathPointPayload[]
nodePathIds?: Array<string | number | null>
@@ -374,6 +419,9 @@ const parseJsonPayload = <T>(payload: unknown, requestUrl: string, contentType:
}
const inFlightRequests = new Map<string, Promise<unknown>>()
// v3 invalidates floor metadata cached before mutable spaces became network-first.
const SGS_SDK_PERSISTENT_CACHE_PREFIX = 'sgs-mobile:sdk-read:v3'
const SGS_SDK_PERSISTENT_CACHE_TTL_MS = 5 * 60 * 1000
const requestJson = <T>(
path: string,
@@ -382,6 +430,7 @@ const requestJson = <T>(
const requestKey = `${options.method || 'GET'}:${path}:${JSON.stringify(options.data || {})}`
const existing = inFlightRequests.get(requestKey)
if (existing) return existing as Promise<T>
const finishPerformance = startGuidePerformance('api', `${options.method || 'GET'} ${path}`)
const request = new Promise<T>((resolve, reject) => {
const baseUrl = resolveAppApiBaseUrl()
@@ -401,6 +450,7 @@ const requestJson = <T>(
const statusCode = Number(response.statusCode || 0)
const contentType = getHeaderValue(response.header as Record<string, string> | undefined, 'content-type')
if (statusCode < 200 || statusCode >= 300) {
finishPerformance('failure', { statusCode })
reject(new Error(`SGS 数据接口请求失败: ${statusCode} ${requestUrl} content-type=${contentType || 'unknown'} body="${previewPayload(response.data)}"`))
return
}
@@ -408,16 +458,24 @@ const requestJson = <T>(
try {
const body = parseJsonPayload<CommonResult<T>>(response.data, requestUrl, contentType)
if (!body || body.code !== 0) {
finishPerformance('failure', { statusCode, code: body?.code, message: body?.msg || '' })
reject(new Error(`SGS 数据接口业务失败: ${requestUrl} code=${body?.code} msg=${body?.msg || ''}`))
return
}
finishPerformance('success', { statusCode })
resolve(body.data as T)
} catch (error) {
finishPerformance('failure', {
error: error instanceof Error ? error.message : String(error)
})
reject(error)
}
},
fail: (error) => {
finishPerformance('failure', {
error: JSON.stringify(error)
})
reject(new Error(`SGS 数据接口网络失败: ${path} ${JSON.stringify(error)}`))
}
})
@@ -427,6 +485,67 @@ const requestJson = <T>(
return request
}
const persistentCacheKeyFor = (path: string) => (
`${SGS_SDK_PERSISTENT_CACHE_PREFIX}:${encodeURIComponent(resolveAppApiBaseUrl())}:${path}`
)
const requestCachedJson = async <T>(path: string): Promise<T> => {
const cacheKey = persistentCacheKeyFor(path)
const cached = readPersistentJsonCache<T>(cacheKey)
if (cached !== null) {
const finishPerformance = startGuidePerformance('api', `GET ${path}`)
finishPerformance('cache-hit', { layer: 'persistent' })
return cached
}
try {
const data = await requestJson<T>(path)
writePersistentJsonCache(cacheKey, data, SGS_SDK_PERSISTENT_CACHE_TTL_MS)
return data
} catch (error) {
const stale = readPersistentJsonCache<T>(cacheKey, true)
if (stale !== null) {
const finishPerformance = startGuidePerformance('api', `GET ${path}`)
finishPerformance('stale-fallback', { layer: 'persistent' })
return stale
}
throw error
}
}
const requestFreshJson = async <T>(path: string): Promise<T> => {
const cacheKey = persistentCacheKeyFor(path)
try {
const data = await requestJson<T>(path)
writePersistentJsonCache(cacheKey, data, SGS_SDK_PERSISTENT_CACHE_TTL_MS)
return data
} catch (error) {
const stale = readPersistentJsonCache<T>(cacheKey, true)
if (stale !== null) {
const finishPerformance = startGuidePerformance('api', `GET ${path}`)
finishPerformance('stale-fallback', { layer: 'persistent' })
return stale
}
throw error
}
}
const requestCachedCollectionJson = async <T>(path: string): Promise<T[]> => {
const cacheKey = persistentCacheKeyFor(path)
const cached = readPersistentJsonCache<T[]>(cacheKey)
if (cached !== null && cached.length > 0) {
const finishPerformance = startGuidePerformance('api', `GET ${path}`)
finishPerformance('cache-hit', { layer: 'persistent' })
return cached
}
// Empty mutable collections are not authoritative across deployments. A
// fresh floor request can upgrade an old persisted [] without broadening the
// query to other floors.
return requestFreshJson<T[]>(path)
}
const buildQueryString = (params: object) => {
const query = new URLSearchParams()
@@ -467,7 +586,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = manifestCache.get(normalizedMapId)
if (cached) return cached
const manifest = await requestJson<SgsSdkManifestPayload>(
const manifest = await requestCachedJson<SgsSdkManifestPayload>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/manifest`
)
manifestCache.set(normalizedMapId, manifest)
@@ -478,7 +597,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = mapDiagnosticsCache.get(normalizedMapId)
if (cached) return cached
const diagnostics = await requestJson<SgsMapDiagnosticsPayload>(
const diagnostics = await requestCachedJson<SgsMapDiagnosticsPayload>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/diagnostics`
)
mapDiagnosticsCache.set(normalizedMapId, diagnostics)
@@ -488,7 +607,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = floorDiagnosticsCache.get(floorId)
if (cached) return cached
const diagnostics = await requestJson<SgsFloorDiagnosticsPayload>(
const diagnostics = await requestCachedJson<SgsFloorDiagnosticsPayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/diagnostics`
)
floorDiagnosticsCache.set(floorId, diagnostics)
@@ -498,37 +617,51 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = floorBundleCache.get(floorId)
if (cached) return cached
const bundle = await requestJson<SgsFloorBundlePayload>(
const bundle = await requestCachedJson<SgsFloorBundlePayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/bundle`
)
floorBundleCache.set(floorId, bundle)
// The floor model and search panel share these authoritative collections.
// Seed the per-floor caches so opening search after a model switch does
// not repeat the two slowest SDK requests.
if (bundle.pois?.length) floorPoiCache.set(floorId, bundle.pois)
if (bundle.spaces?.length) floorSpaceCache.set(floorId, bundle.spaces)
// Bundle snapshots are authoritative even for an empty collection. This
// differs from the standalone mutable endpoints, where an empty response
// remains retryable to recover from a stale browser cache.
if (Array.isArray(bundle.businessPois)) {
floorBusinessPoiCache.set(`${floorId}:${stableCacheKey({})}`, bundle.businessPois)
}
if (Array.isArray(bundle.navigablePlaces)) {
navigablePlaceCache.set(floorId, bundle.navigablePlaces)
}
return bundle
},
async getFloorPois(floorId) {
const cached = floorPoiCache.get(floorId)
if (cached) return cached
if (cached?.length) return cached
const pois = await requestJson<SgsPoiPayload[]>(
const pois = await requestCachedCollectionJson<SgsPoiPayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/pois`
)
floorPoiCache.set(floorId, pois)
if (pois.length) floorPoiCache.set(floorId, pois)
return pois
},
async getFloorSpaces(floorId) {
const cached = floorSpaceCache.get(floorId)
if (cached) return cached
if (cached?.length) return cached
const spaces = await requestJson<SgsSpacePayload[]>(
const spaces = await requestFreshJson<SgsSpacePayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/spaces`
)
floorSpaceCache.set(floorId, spaces)
if (spaces.length) floorSpaceCache.set(floorId, spaces)
return spaces
},
async getGuideStops(floorId) {
const cached = floorGuideStopCache.get(floorId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
const guideStops = await requestCachedJson<SgsGuideStopPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/guide-stops`
)
floorGuideStopCache.set(floorId, guideStops)
@@ -538,31 +671,29 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = hallGuideStopCache.get(hallId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
const guideStops = await requestCachedJson<SgsGuideStopPayload[]>(
`/gis/sdk/halls/${hallIdFor(hallId)}/guide-stops`
)
hallGuideStopCache.set(hallId, guideStops)
return guideStops
},
async getNavigablePlaces(floorId) {
const cached = navigablePlaceCache.get(floorId)
if (cached) return cached
if (navigablePlaceCache.has(floorId)) return navigablePlaceCache.get(floorId) || []
const places = await requestJson<SgsNavigablePlacePayload[]>(
const places = await requestCachedCollectionJson<SgsNavigablePlacePayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/navigable-places`
)
navigablePlaceCache.set(floorId, places)
if (places.length) navigablePlaceCache.set(floorId, places)
return places
},
async getFloorBusinessPois(floorId, options = {}) {
const cacheKey = `${floorId}:${stableCacheKey(options)}`
const cached = floorBusinessPoiCache.get(cacheKey)
if (cached) return cached
if (floorBusinessPoiCache.has(cacheKey)) return floorBusinessPoiCache.get(cacheKey) || []
const pois = await requestJson<SgsPoiPayload[]>(
const pois = await requestCachedCollectionJson<SgsPoiPayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/business-pois${buildQueryString(options)}`
)
floorBusinessPoiCache.set(cacheKey, pois)
if (pois.length) floorBusinessPoiCache.set(cacheKey, pois)
return pois
},
async queryPois(params) {
@@ -570,7 +701,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = poiQueryCache.get(cacheKey)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
const pois = await requestCachedJson<SgsPoiPayload[]>(
`/gis/sdk/pois${buildQueryString(params)}`
)
poiQueryCache.set(cacheKey, pois)
@@ -582,7 +713,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = featuredRoutesCache.get(cacheKey)
if (cached) return cached
const routes = await requestJson<SgsFeaturedRouteSummaryPayload[]>(
const routes = await requestCachedJson<SgsFeaturedRouteSummaryPayload[]>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/featured-routes${buildQueryString(options)}`
)
featuredRoutesCache.set(cacheKey, routes)
@@ -592,7 +723,7 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const cached = featuredRouteDetailCache.get(routeId)
if (cached) return cached
const route = await requestJson<SgsFeaturedRouteDetailPayload>(
const route = await requestCachedJson<SgsFeaturedRouteDetailPayload>(
`/gis/sdk/featured-routes/${encodeURIComponent(routeId)}`
)
featuredRouteDetailCache.set(routeId, route)

View File

@@ -27,7 +27,6 @@ export interface StaticNavPoiPayload {
sourceObjectName?: string
navigationReadiness?: string
sourceConfidence?: string
visitorVisible?: boolean | null
}
export interface StaticNavManifestFloorModelPayload {