推进活动系统最小成品闭环与游客体验
This commit is contained in:
@@ -63,12 +63,92 @@ export interface BackendPresentationSummary {
|
||||
version?: string | null
|
||||
}
|
||||
|
||||
export interface BackendPreviewControlSummary {
|
||||
id?: string | null
|
||||
label?: string | null
|
||||
kind?: string | null
|
||||
lon?: number | null
|
||||
lat?: number | null
|
||||
}
|
||||
|
||||
export interface BackendPreviewLegSummary {
|
||||
fromLon?: number | null
|
||||
fromLat?: number | null
|
||||
toLon?: number | null
|
||||
toLat?: number | null
|
||||
}
|
||||
|
||||
export interface BackendPreviewVariantSummary {
|
||||
variantId?: string | null
|
||||
id?: string | null
|
||||
name?: string | null
|
||||
routeCode?: string | null
|
||||
controls?: BackendPreviewControlSummary[] | null
|
||||
legs?: BackendPreviewLegSummary[] | null
|
||||
}
|
||||
|
||||
export interface BackendPreviewSummary {
|
||||
mode?: string | null
|
||||
baseTiles?: {
|
||||
tileBaseUrl?: string | null
|
||||
zoom?: number | null
|
||||
tileSize?: number | null
|
||||
} | null
|
||||
viewport?: {
|
||||
width?: number | null
|
||||
height?: number | null
|
||||
minLon?: number | null
|
||||
minLat?: number | null
|
||||
maxLon?: number | null
|
||||
maxLat?: number | null
|
||||
} | null
|
||||
variants?: BackendPreviewVariantSummary[] | null
|
||||
selectedVariantId?: string | null
|
||||
}
|
||||
|
||||
export interface BackendContentBundleSummary {
|
||||
bundleId?: string | null
|
||||
bundleType?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
|
||||
export interface BackendExperienceMapSummary {
|
||||
placeId?: string | null
|
||||
placeName?: string | null
|
||||
mapId?: string | null
|
||||
mapName?: string | null
|
||||
coverUrl?: string | null
|
||||
summary?: string | null
|
||||
defaultExperienceCount?: number | null
|
||||
defaultExperienceEventIds?: string[] | null
|
||||
}
|
||||
|
||||
export interface BackendDefaultExperienceSummary {
|
||||
eventId?: string | null
|
||||
title?: string | null
|
||||
subtitle?: string | null
|
||||
eventType?: string | null
|
||||
status?: string | null
|
||||
statusCode?: string | null
|
||||
ctaText?: string | null
|
||||
isDefaultExperience?: boolean
|
||||
showInEventList?: boolean
|
||||
currentPresentation?: BackendPresentationSummary | null
|
||||
currentContentBundle?: BackendContentBundleSummary | null
|
||||
}
|
||||
|
||||
export interface BackendExperienceMapDetail {
|
||||
placeId?: string | null
|
||||
placeName?: string | null
|
||||
mapId?: string | null
|
||||
mapName?: string | null
|
||||
coverUrl?: string | null
|
||||
summary?: string | null
|
||||
tileBaseUrl?: string | null
|
||||
tileMetaUrl?: string | null
|
||||
defaultExperiences?: BackendDefaultExperienceSummary[] | null
|
||||
}
|
||||
|
||||
export interface BackendEntrySessionSummary {
|
||||
id: string
|
||||
status: string
|
||||
@@ -151,6 +231,7 @@ export interface BackendEventPlayResult {
|
||||
}
|
||||
currentPresentation?: BackendPresentationSummary | null
|
||||
currentContentBundle?: BackendContentBundleSummary | null
|
||||
preview?: BackendPreviewSummary | null
|
||||
release?: {
|
||||
id: string
|
||||
configLabel: string
|
||||
@@ -188,6 +269,7 @@ export interface BackendLaunchResult {
|
||||
}
|
||||
business: {
|
||||
source: string
|
||||
isGuest?: boolean
|
||||
eventId: string
|
||||
sessionId: string
|
||||
sessionToken: string
|
||||
@@ -348,6 +430,17 @@ export function getEventPlay(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export function getPublicEventPlay(input: {
|
||||
baseUrl: string
|
||||
eventId: string
|
||||
}): Promise<BackendEventPlayResult> {
|
||||
return requestBackend<BackendEventPlayResult>({
|
||||
method: 'GET',
|
||||
baseUrl: input.baseUrl,
|
||||
path: `/public/events/${encodeURIComponent(input.eventId)}/play`,
|
||||
})
|
||||
}
|
||||
|
||||
export function getEntryHome(input: {
|
||||
baseUrl: string
|
||||
accessToken: string
|
||||
@@ -391,6 +484,32 @@ export function launchEvent(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export function launchPublicEvent(input: {
|
||||
baseUrl: string
|
||||
eventId: string
|
||||
releaseId?: string
|
||||
variantId?: string
|
||||
clientType: string
|
||||
deviceKey: string
|
||||
}): Promise<BackendLaunchResult> {
|
||||
const body: Record<string, unknown> = {
|
||||
clientType: input.clientType,
|
||||
deviceKey: input.deviceKey,
|
||||
}
|
||||
if (input.releaseId) {
|
||||
body.releaseId = input.releaseId
|
||||
}
|
||||
if (input.variantId) {
|
||||
body.variantId = input.variantId
|
||||
}
|
||||
return requestBackend<BackendLaunchResult>({
|
||||
method: 'POST',
|
||||
baseUrl: input.baseUrl,
|
||||
path: `/public/events/${encodeURIComponent(input.eventId)}/launch`,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function startSession(input: {
|
||||
baseUrl: string
|
||||
sessionId: string
|
||||
@@ -463,3 +582,49 @@ export function postClientLog(input: {
|
||||
body: input.payload as unknown as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
export function getExperienceMaps(input: {
|
||||
baseUrl: string
|
||||
accessToken: string
|
||||
}): Promise<BackendExperienceMapSummary[]> {
|
||||
return requestBackend<BackendExperienceMapSummary[]>({
|
||||
method: 'GET',
|
||||
baseUrl: input.baseUrl,
|
||||
path: '/experience-maps',
|
||||
authToken: input.accessToken,
|
||||
})
|
||||
}
|
||||
|
||||
export function getPublicExperienceMaps(input: {
|
||||
baseUrl: string
|
||||
}): Promise<BackendExperienceMapSummary[]> {
|
||||
return requestBackend<BackendExperienceMapSummary[]>({
|
||||
method: 'GET',
|
||||
baseUrl: input.baseUrl,
|
||||
path: '/public/experience-maps',
|
||||
})
|
||||
}
|
||||
|
||||
export function getExperienceMapDetail(input: {
|
||||
baseUrl: string
|
||||
accessToken: string
|
||||
mapAssetId: string
|
||||
}): Promise<BackendExperienceMapDetail> {
|
||||
return requestBackend<BackendExperienceMapDetail>({
|
||||
method: 'GET',
|
||||
baseUrl: input.baseUrl,
|
||||
path: `/experience-maps/${encodeURIComponent(input.mapAssetId)}`,
|
||||
authToken: input.accessToken,
|
||||
})
|
||||
}
|
||||
|
||||
export function getPublicExperienceMapDetail(input: {
|
||||
baseUrl: string
|
||||
mapAssetId: string
|
||||
}): Promise<BackendExperienceMapDetail> {
|
||||
return requestBackend<BackendExperienceMapDetail>({
|
||||
method: 'GET',
|
||||
baseUrl: input.baseUrl,
|
||||
path: `/public/experience-maps/${encodeURIComponent(input.mapAssetId)}`,
|
||||
})
|
||||
}
|
||||
|
||||
482
miniprogram/utils/prepareMapPreview.ts
Normal file
482
miniprogram/utils/prepareMapPreview.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
import { type BackendPreviewSummary } from './backendApi'
|
||||
import { lonLatToWorldTile, type LonLatPoint } from './projection'
|
||||
import { isTileWithinBounds, type RemoteMapConfig } from './remoteMapConfig'
|
||||
import { buildTileUrl } from './tile'
|
||||
|
||||
export interface PreparePreviewTile {
|
||||
url: string
|
||||
x: number
|
||||
y: number
|
||||
leftPx: number
|
||||
topPx: number
|
||||
sizePx: number
|
||||
}
|
||||
|
||||
export interface PreparePreviewControl {
|
||||
kind: 'start' | 'control' | 'finish'
|
||||
label: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface PreparePreviewLeg {
|
||||
fromX: number
|
||||
fromY: number
|
||||
toX: number
|
||||
toY: number
|
||||
}
|
||||
|
||||
export interface PreparePreviewScene {
|
||||
width: number
|
||||
height: number
|
||||
zoom: number
|
||||
tiles: PreparePreviewTile[]
|
||||
controls: PreparePreviewControl[]
|
||||
legs: PreparePreviewLeg[]
|
||||
overlayAvailable: boolean
|
||||
}
|
||||
|
||||
interface PreviewPointSeed {
|
||||
kind: 'start' | 'control' | 'finish'
|
||||
label: string
|
||||
point: LonLatPoint
|
||||
}
|
||||
|
||||
function resolvePreviewTileTemplate(tileBaseUrl: string): string {
|
||||
if (tileBaseUrl.indexOf('{z}') >= 0 && tileBaseUrl.indexOf('{x}') >= 0 && tileBaseUrl.indexOf('{y}') >= 0) {
|
||||
return tileBaseUrl
|
||||
}
|
||||
const normalizedBase = tileBaseUrl.replace(/\/+$/, '')
|
||||
return `${normalizedBase}/{z}/{x}/{y}.png`
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
function collectCoursePoints(config: RemoteMapConfig): LonLatPoint[] {
|
||||
if (!config.course) {
|
||||
return []
|
||||
}
|
||||
|
||||
const points: LonLatPoint[] = []
|
||||
config.course.layers.starts.forEach((item) => {
|
||||
points.push(item.point)
|
||||
})
|
||||
config.course.layers.controls.forEach((item) => {
|
||||
points.push(item.point)
|
||||
})
|
||||
config.course.layers.finishes.forEach((item) => {
|
||||
points.push(item.point)
|
||||
})
|
||||
return points
|
||||
}
|
||||
|
||||
function collectPreviewPointSeeds(items: Array<{
|
||||
kind?: string | null
|
||||
label?: string | null
|
||||
lon?: number | null
|
||||
lat?: number | null
|
||||
}>): PreviewPointSeed[] {
|
||||
const seeds: PreviewPointSeed[] = []
|
||||
items.forEach((item, index) => {
|
||||
if (typeof item.lon !== 'number' || typeof item.lat !== 'number') {
|
||||
return
|
||||
}
|
||||
const kind = item.kind === 'start' || item.kind === 'finish' ? item.kind : 'control'
|
||||
seeds.push({
|
||||
kind,
|
||||
label: item.label || String(index + 1),
|
||||
point: {
|
||||
lon: item.lon,
|
||||
lat: item.lat,
|
||||
},
|
||||
})
|
||||
})
|
||||
return seeds
|
||||
}
|
||||
|
||||
function computePointBounds(points: LonLatPoint[]): { minLon: number; minLat: number; maxLon: number; maxLat: number } | null {
|
||||
if (!points.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
let minLon = points[0].lon
|
||||
let maxLon = points[0].lon
|
||||
let minLat = points[0].lat
|
||||
let maxLat = points[0].lat
|
||||
points.forEach((point) => {
|
||||
minLon = Math.min(minLon, point.lon)
|
||||
maxLon = Math.max(maxLon, point.lon)
|
||||
minLat = Math.min(minLat, point.lat)
|
||||
maxLat = Math.max(maxLat, point.lat)
|
||||
})
|
||||
|
||||
return {
|
||||
minLon,
|
||||
minLat,
|
||||
maxLon,
|
||||
maxLat,
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePreviewZoom(config: RemoteMapConfig, width: number, height: number, points: LonLatPoint[]): number {
|
||||
const upperZoom = clamp(config.defaultZoom > 0 ? config.defaultZoom : config.maxZoom, config.minZoom, config.maxZoom)
|
||||
if (!points.length) {
|
||||
return clamp(upperZoom - 1, config.minZoom, config.maxZoom)
|
||||
}
|
||||
|
||||
const bounds = computePointBounds(points)
|
||||
if (!bounds) {
|
||||
return clamp(upperZoom - 1, config.minZoom, config.maxZoom)
|
||||
}
|
||||
|
||||
let fittedZoom = config.minZoom
|
||||
for (let zoom = upperZoom; zoom >= config.minZoom; zoom -= 1) {
|
||||
const northWest = lonLatToWorldTile({ lon: bounds.minLon, lat: bounds.maxLat }, zoom)
|
||||
const southEast = lonLatToWorldTile({ lon: bounds.maxLon, lat: bounds.minLat }, zoom)
|
||||
const widthPx = Math.abs(southEast.x - northWest.x) * config.tileSize
|
||||
const heightPx = Math.abs(southEast.y - northWest.y) * config.tileSize
|
||||
if (widthPx <= width * 0.9 && heightPx <= height * 0.9) {
|
||||
fittedZoom = zoom
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return clamp(fittedZoom, config.minZoom, config.maxZoom)
|
||||
}
|
||||
|
||||
function resolvePreviewCenter(config: RemoteMapConfig, zoom: number, points: LonLatPoint[]): { x: number; y: number } {
|
||||
const bounds = computePointBounds(points)
|
||||
if (bounds) {
|
||||
const center = lonLatToWorldTile(
|
||||
{
|
||||
lon: (bounds.minLon + bounds.maxLon) / 2,
|
||||
lat: (bounds.minLat + bounds.maxLat) / 2,
|
||||
},
|
||||
zoom,
|
||||
)
|
||||
return {
|
||||
x: center.x,
|
||||
y: center.y,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: config.initialCenterTileX,
|
||||
y: config.initialCenterTileY,
|
||||
}
|
||||
}
|
||||
|
||||
function buildPreviewTiles(
|
||||
config: RemoteMapConfig,
|
||||
zoom: number,
|
||||
width: number,
|
||||
height: number,
|
||||
centerWorldX: number,
|
||||
centerWorldY: number,
|
||||
): PreparePreviewTile[] {
|
||||
const halfWidthInTiles = width / 2 / config.tileSize
|
||||
const halfHeightInTiles = height / 2 / config.tileSize
|
||||
const minTileX = Math.floor(centerWorldX - halfWidthInTiles) - 1
|
||||
const maxTileX = Math.ceil(centerWorldX + halfWidthInTiles) + 1
|
||||
const minTileY = Math.floor(centerWorldY - halfHeightInTiles) - 1
|
||||
const maxTileY = Math.ceil(centerWorldY + halfHeightInTiles) + 1
|
||||
const tiles: PreparePreviewTile[] = []
|
||||
|
||||
for (let tileY = minTileY; tileY <= maxTileY; tileY += 1) {
|
||||
for (let tileX = minTileX; tileX <= maxTileX; tileX += 1) {
|
||||
if (!isTileWithinBounds(config.tileBoundsByZoom, zoom, tileX, tileY)) {
|
||||
continue
|
||||
}
|
||||
|
||||
tiles.push({
|
||||
url: buildTileUrl(config.tileSource, zoom, tileX, tileY),
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
leftPx: Math.round(width / 2 + (tileX - centerWorldX) * config.tileSize),
|
||||
topPx: Math.round(height / 2 + (tileY - centerWorldY) * config.tileSize),
|
||||
sizePx: config.tileSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tiles
|
||||
}
|
||||
|
||||
function applyFitTransform(
|
||||
scene: PreparePreviewScene,
|
||||
paddingRatio: number,
|
||||
): PreparePreviewScene {
|
||||
if (!scene.controls.length) {
|
||||
return scene
|
||||
}
|
||||
|
||||
let minX = scene.controls[0].x
|
||||
let maxX = scene.controls[0].x
|
||||
let minY = scene.controls[0].y
|
||||
let maxY = scene.controls[0].y
|
||||
|
||||
scene.controls.forEach((control) => {
|
||||
minX = Math.min(minX, control.x)
|
||||
maxX = Math.max(maxX, control.x)
|
||||
minY = Math.min(minY, control.y)
|
||||
maxY = Math.max(maxY, control.y)
|
||||
})
|
||||
|
||||
const boundsWidth = Math.max(1, maxX - minX)
|
||||
const boundsHeight = Math.max(1, maxY - minY)
|
||||
const targetWidth = scene.width * paddingRatio
|
||||
const targetHeight = scene.height * paddingRatio
|
||||
const scale = Math.max(1, Math.min(targetWidth / boundsWidth, targetHeight / boundsHeight))
|
||||
const centerX = (minX + maxX) / 2
|
||||
const centerY = (minY + maxY) / 2
|
||||
|
||||
const transformX = (value: number) => ((value - centerX) * scale) + scene.width / 2
|
||||
const transformY = (value: number) => ((value - centerY) * scale) + scene.height / 2
|
||||
|
||||
return {
|
||||
...scene,
|
||||
tiles: scene.tiles.map((tile) => ({
|
||||
...tile,
|
||||
leftPx: transformX(tile.leftPx),
|
||||
topPx: transformY(tile.topPx),
|
||||
sizePx: tile.sizePx * scale,
|
||||
})),
|
||||
controls: scene.controls.map((control) => ({
|
||||
...control,
|
||||
x: transformX(control.x),
|
||||
y: transformY(control.y),
|
||||
})),
|
||||
legs: scene.legs.map((leg) => ({
|
||||
fromX: transformX(leg.fromX),
|
||||
fromY: transformY(leg.fromY),
|
||||
toX: transformX(leg.toX),
|
||||
toY: transformY(leg.toY),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPreparePreviewScene(
|
||||
config: RemoteMapConfig,
|
||||
width: number,
|
||||
height: number,
|
||||
overlayEnabled: boolean,
|
||||
): PreparePreviewScene {
|
||||
const normalizedWidth = Math.max(240, Math.round(width))
|
||||
const normalizedHeight = Math.max(140, Math.round(height))
|
||||
const points = collectCoursePoints(config)
|
||||
const zoom = resolvePreviewZoom(config, normalizedWidth, normalizedHeight, points)
|
||||
const center = resolvePreviewCenter(config, zoom, points)
|
||||
const tiles = buildPreviewTiles(config, zoom, normalizedWidth, normalizedHeight, center.x, center.y)
|
||||
|
||||
const controls: PreparePreviewControl[] = []
|
||||
const legs: PreparePreviewLeg[] = []
|
||||
|
||||
if (overlayEnabled && config.course) {
|
||||
const projectPoint = (point: LonLatPoint) => {
|
||||
const world = lonLatToWorldTile(point, zoom)
|
||||
return {
|
||||
x: normalizedWidth / 2 + (world.x - center.x) * config.tileSize,
|
||||
y: normalizedHeight / 2 + (world.y - center.y) * config.tileSize,
|
||||
}
|
||||
}
|
||||
|
||||
config.course.layers.legs.forEach((leg) => {
|
||||
const from = projectPoint(leg.fromPoint)
|
||||
const to = projectPoint(leg.toPoint)
|
||||
legs.push({
|
||||
fromX: from.x,
|
||||
fromY: from.y,
|
||||
toX: to.x,
|
||||
toY: to.y,
|
||||
})
|
||||
})
|
||||
|
||||
config.course.layers.starts.forEach((item) => {
|
||||
const point = projectPoint(item.point)
|
||||
controls.push({
|
||||
kind: 'start',
|
||||
label: item.label,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
})
|
||||
})
|
||||
config.course.layers.controls.forEach((item) => {
|
||||
const point = projectPoint(item.point)
|
||||
controls.push({
|
||||
kind: 'control',
|
||||
label: item.label,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
})
|
||||
})
|
||||
config.course.layers.finishes.forEach((item) => {
|
||||
const point = projectPoint(item.point)
|
||||
controls.push({
|
||||
kind: 'finish',
|
||||
label: item.label,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const baseScene: PreparePreviewScene = {
|
||||
width: normalizedWidth,
|
||||
height: normalizedHeight,
|
||||
zoom,
|
||||
tiles,
|
||||
controls,
|
||||
legs,
|
||||
overlayAvailable: overlayEnabled && !!config.course,
|
||||
}
|
||||
|
||||
return applyFitTransform(baseScene, 0.88)
|
||||
}
|
||||
|
||||
export function buildPreparePreviewSceneFromVariantControls(
|
||||
config: RemoteMapConfig,
|
||||
width: number,
|
||||
height: number,
|
||||
controlsInput: Array<{
|
||||
kind?: string | null
|
||||
label?: string | null
|
||||
lon?: number | null
|
||||
lat?: number | null
|
||||
}>,
|
||||
): PreparePreviewScene | null {
|
||||
const seeds = collectPreviewPointSeeds(controlsInput)
|
||||
if (!seeds.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedWidth = Math.max(240, Math.round(width))
|
||||
const normalizedHeight = Math.max(140, Math.round(height))
|
||||
const points = seeds.map((item) => item.point)
|
||||
const zoom = resolvePreviewZoom(config, normalizedWidth, normalizedHeight, points)
|
||||
const center = resolvePreviewCenter(config, zoom, points)
|
||||
const tiles = buildPreviewTiles(config, zoom, normalizedWidth, normalizedHeight, center.x, center.y)
|
||||
|
||||
const controls: PreparePreviewControl[] = seeds.map((item) => {
|
||||
const world = lonLatToWorldTile(item.point, zoom)
|
||||
return {
|
||||
kind: item.kind,
|
||||
label: item.label,
|
||||
x: normalizedWidth / 2 + (world.x - center.x) * config.tileSize,
|
||||
y: normalizedHeight / 2 + (world.y - center.y) * config.tileSize,
|
||||
}
|
||||
})
|
||||
|
||||
const scene: PreparePreviewScene = {
|
||||
width: normalizedWidth,
|
||||
height: normalizedHeight,
|
||||
zoom,
|
||||
tiles,
|
||||
controls,
|
||||
legs: [],
|
||||
overlayAvailable: true,
|
||||
}
|
||||
|
||||
return applyFitTransform(scene, 0.88)
|
||||
}
|
||||
|
||||
export function buildPreparePreviewSceneFromBackendPreview(
|
||||
preview: BackendPreviewSummary,
|
||||
width: number,
|
||||
height: number,
|
||||
variantId?: string | null,
|
||||
tileUrlTemplateOverride?: string | null,
|
||||
): PreparePreviewScene | null {
|
||||
if (!preview.baseTiles || !preview.viewport || !preview.baseTiles.tileBaseUrl || typeof preview.baseTiles.zoom !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
const viewport = preview.viewport
|
||||
if (
|
||||
typeof viewport.minLon !== 'number'
|
||||
|| typeof viewport.minLat !== 'number'
|
||||
|| typeof viewport.maxLon !== 'number'
|
||||
|| typeof viewport.maxLat !== 'number'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedWidth = Math.max(240, Math.round(width))
|
||||
const normalizedHeight = Math.max(140, Math.round(height))
|
||||
const zoom = Math.round(preview.baseTiles.zoom)
|
||||
const tileSize = typeof preview.baseTiles.tileSize === 'number' && preview.baseTiles.tileSize > 0
|
||||
? preview.baseTiles.tileSize
|
||||
: 256
|
||||
const template = resolvePreviewTileTemplate(tileUrlTemplateOverride || preview.baseTiles.tileBaseUrl)
|
||||
|
||||
const center = lonLatToWorldTile(
|
||||
{
|
||||
lon: (viewport.minLon + viewport.maxLon) / 2,
|
||||
lat: (viewport.minLat + viewport.maxLat) / 2,
|
||||
},
|
||||
zoom,
|
||||
)
|
||||
|
||||
const northWest = lonLatToWorldTile({ lon: viewport.minLon, lat: viewport.maxLat }, zoom)
|
||||
const southEast = lonLatToWorldTile({ lon: viewport.maxLon, lat: viewport.minLat }, zoom)
|
||||
const boundsWidthPx = Math.max(1, Math.abs(southEast.x - northWest.x) * tileSize)
|
||||
const boundsHeightPx = Math.max(1, Math.abs(southEast.y - northWest.y) * tileSize)
|
||||
const scale = Math.min(normalizedWidth / boundsWidthPx, normalizedHeight / boundsHeightPx)
|
||||
|
||||
const minTileX = Math.floor(Math.min(northWest.x, southEast.x)) - 1
|
||||
const maxTileX = Math.ceil(Math.max(northWest.x, southEast.x)) + 1
|
||||
const minTileY = Math.floor(Math.min(northWest.y, southEast.y)) - 1
|
||||
const maxTileY = Math.ceil(Math.max(northWest.y, southEast.y)) + 1
|
||||
|
||||
const tiles: PreparePreviewTile[] = []
|
||||
for (let tileY = minTileY; tileY <= maxTileY; tileY += 1) {
|
||||
for (let tileX = minTileX; tileX <= maxTileX; tileX += 1) {
|
||||
const leftPx = ((tileX - center.x) * tileSize * scale) + normalizedWidth / 2
|
||||
const topPx = ((tileY - center.y) * tileSize * scale) + normalizedHeight / 2
|
||||
tiles.push({
|
||||
url: buildTileUrl(template, zoom, tileX, tileY),
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
leftPx,
|
||||
topPx,
|
||||
sizePx: tileSize * scale,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedVariantId = variantId || preview.selectedVariantId || ''
|
||||
const previewVariant = (preview.variants || []).find((item) => {
|
||||
const candidateId = item.variantId || item.id || ''
|
||||
return candidateId === normalizedVariantId
|
||||
}) || (preview.variants && preview.variants[0] ? preview.variants[0] : null)
|
||||
|
||||
const controls: PreparePreviewControl[] = []
|
||||
if (previewVariant && previewVariant.controls && previewVariant.controls.length) {
|
||||
previewVariant.controls.forEach((item, index) => {
|
||||
if (typeof item.lon !== 'number' || typeof item.lat !== 'number') {
|
||||
return
|
||||
}
|
||||
const world = lonLatToWorldTile({ lon: item.lon, lat: item.lat }, zoom)
|
||||
const x = ((world.x - center.x) * tileSize * scale) + normalizedWidth / 2
|
||||
const y = ((world.y - center.y) * tileSize * scale) + normalizedHeight / 2
|
||||
const normalizedKind = item.kind === 'start' || item.kind === 'finish' ? item.kind : 'control'
|
||||
controls.push({
|
||||
kind: normalizedKind,
|
||||
label: item.label || String(index + 1),
|
||||
x,
|
||||
y,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
width: normalizedWidth,
|
||||
height: normalizedHeight,
|
||||
zoom,
|
||||
tiles,
|
||||
controls,
|
||||
legs: [],
|
||||
overlayAvailable: controls.length > 0,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user