第05章 全景与地图双向联动机制

5.1 联动概述

map-360-demo 的核心亮点是全景图与地图的双向联动。所谓”双向”指的是:

  1. 全景 → 地图:点击全景中的标记热点,地图上对应的热点会高亮
  2. 地图 → 全景:点击地图上的热点,全景会自动旋转到对应位置

这种联动通过 Photo Sphere Viewer 的 PlanPlugin 实现,其核心机制是标记的 data.plan 字段。

5.2 联动核心:data.plan 字段

5.2.1 标记配置转换

src/data/scenes.ts 中,toPsvMarkerConfig 函数将业务标记数据转换为 PSV 标记配置:

// scenes.ts:117-140
export function toPsvMarkerConfig(m: MarkerData): PsvMarkerConfig {
  const isLink = m.type === 'link'
  const safeName = escapeHtml(m.name)
  const safeDesc = escapeHtml(m.description)
  const tooltip = isLink ? `${safeName}(跳转)` : safeName
  return {
    id: m.id,
    tooltip,
    content: `<div class="psv-marker-content"><h3>${safeName}</h3><p>${safeDesc}</p></div>`,
    position: m.position,          // 全景球形坐标
    image: isLink ? LINK_ICON : BASE_URL + 'pictos/pin-blue.png',
    size: { width: 32, height: 32 },
    anchor: 'bottom center',
    className: isLink ? 'psv-marker--link' : '',
    data: {
      plan: {                       // PlanPlugin 联动字段
        coordinates: m.coordinates, // GPS 经纬度
        size: 28,
        image: isLink ? LINK_ICON : BASE_URL + 'pictos/pin-blue.png',
      },
    },
  }
}

5.2.2 联动原理

data.plan 字段是 PlanPlugin 读取地图热点的约定:

  • position(球形坐标 yaw/pitch):决定标记在全景中的位置
  • data.plan.coordinates(GPS 经纬度):决定标记在地图上的位置

两者通过同一个 marker id 关联。PlanPlugin 会自动:

  1. 在全景中渲染标记(根据 position
  2. 在地图上渲染热点(根据 data.plan.coordinates
  3. 建立两者的关联关系

5.3 点击事件分发

5.3.1 统一事件出口

PsvContainer.vue 中,全景标记点击和地图热点点击都 emit 同一个 marker-click 事件:

// PsvContainer.vue:123-131
markersPlugin?.addEventListener('select-marker', ({ marker }) => {
  emit('marker-click', marker.id)
})

planPlugin?.addEventListener('select-hotspot', ({ hotspotId }) => {
  emit('marker-click', hotspotId)
})

5.3.2 App 统一分发

App.vue 中,onMarkerClick 根据标记类型决定行为:

// App.vue:146-164
function onMarkerClick(id: string) {
  const marker = currentMarkers.value.find(m => m.id === id)
  if (!marker) return
  if (marker.type === 'link' && marker.targetSceneId) {
    // link 标记 → 切换场景
    if (marker.targetSceneId === currentScene.value.id) {
      show('已在该场景中', 'info'); return
    }
    flow.exitAdding()
    const target = scenes.value.find(s => s.id === marker.targetSceneId)
    switchScene(marker.targetSceneId!)
    if (target) show(`已跳转至「${target.name}」`, 'info')
  } else {
    // info 标记 → 旋转视角
    psvRef.value?.gotoMarker(id)
  }
}

核心逻辑

  • link 标记:切换场景(调用 switchScene
  • info 标记:旋转视角(调用 gotoMarker

5.4 全景旋转:gotoMarker

5.4.1 暴露接口

PsvContainer.vue 通过 defineExpose 暴露 gotoMarker

// PsvContainer.vue:210-218
defineExpose({ gotoMarker, refreshMarkers })

5.4.2 内部实现

function gotoMarker(id: string) {
  markersPlugin?.gotoMarker(id)
}

gotoMarker 内部调用 MarkersPlugin 的 gotoMarker 方法,该方法会:

  1. 将全景视角旋转到标记所在位置
  2. 高亮该标记

这就是点击标记触发全景旋转的入口。

5.5 场景切换时的联动更新

当切换场景时,PlanPlugin 需要同步更新地图配置:

// PsvContainer.vue:175-187
watch(() => props.scene.id, (id) => {
  if (!viewer || viewerSceneId === id) return
  viewerSceneId = id
  planPlugin?.setOptions({ bearing: props.scene.bearing })
  planPlugin?.setCoordinates(props.scene.coordinates)
  planPlugin?.setZoom(props.scene.defaultZoom)
  viewer.setPanorama(props.scene.panorama, {
    caption: props.scene.name,
    showLoader: true,
  })
})

切换场景时:

  1. 更新地图方位角(setOptions
  2. 更新地图中心坐标(setCoordinates
  3. 更新地图缩放级别(setZoom
  4. 更新全景图(setPanorama

5.6 联动时序图

用户点击全景标记
    │
    ▼
PsvContainer: select-marker 事件
    │
    ▼
emit('marker-click', id)
    │
    ▼
App.vue: onMarkerClick(id)
    │
    ├── link 标记 → switchScene(targetSceneId)
    │       │
    │       └── PsvContainer watcher 检测 scene.id 变化
    │               └── 更新地图 + 全景
    │
    └── info 标记 → psvRef.gotoMarker(id)
            │
            └── markersPlugin.gotoMarker(id)
                    └── 全景旋转到标记位置

5.7 联动机制总结

方向 触发方式 处理逻辑
全景 → 地图 点击全景标记 PlanPlugin 自动高亮地图热点
地图 → 全景 点击地图热点 emit marker-click → gotoMarker 旋转视角
场景切换 切换场景 更新地图坐标/方位角/缩放 + 更新全景图

关键设计

  1. 统一事件出口:全景和地图的点击都 emit marker-click,由 App 统一分发
  2. data.plan 字段:通过标记的 data.plan 字段自动关联全景与地图位置
  3. 复用 viewer:场景切换复用 viewer,保留 WebGL 上下文和瓦片缓存

5.8 下一步

理解了联动机制后,进入 第06章 标记管理功能详解 了解标记的完整管理流程。