高德地图添加轨迹规划和重放功能
This commit is contained in:
200
docs/interview-question-route-track.md
Normal file
200
docs/interview-question-route-track.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 前端面试题:地图路线规划 + 轨迹动画系统
|
||||
|
||||
## 题目描述
|
||||
|
||||
在 Vue 3 + 高德地图的养老/医疗微前端应用中,实现一个**护理员轨迹系统**:
|
||||
|
||||
```
|
||||
操作流程:
|
||||
[📍标记起点] → 点击地图放 📍
|
||||
[🏁标记终点] → 点击地图放 🏁
|
||||
[➕添加护理员] → 在起点创建 👩⚕️
|
||||
[🚗规划路线] → AMap.Driving 计算驾车路线 → 蓝色折线
|
||||
[▶出发] → 护理员沿路线动画移动
|
||||
|
||||
不管路径 500km 还是 2000km,动画始终 ~10 秒丝滑播完
|
||||
```
|
||||
|
||||
**核心要求:**
|
||||
|
||||
1. 起点和终点通过**点击地图手动标记**(独立 `AMap.Marker`)
|
||||
2. 驾车路线使用 `AMap.Driving` 规划(真实道路路径,非直线)
|
||||
3. 护理员标记沿路线**动画移动**,无论路径多长都流畅无卡顿
|
||||
4. 支持暂停、回到起点、全部清除
|
||||
5. 护理员不存在时点击"出发"要**自动创建**(容错 UX)
|
||||
|
||||
---
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
useRouteTrack(mapInstance)
|
||||
├─ enterSetMode('origin'|'dest') ← 点击地图放标记
|
||||
├─ addCaregiver() ← 在起点创建护理员
|
||||
├─ planRoute() ← AMap.Driving 路线规划
|
||||
│ └─ downsamplePath(full, 250) ← ★ 路径下采样
|
||||
├─ startAnimation() ← setInterval 帧动画
|
||||
└─ clearAll() ← 完整清理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心技术点
|
||||
|
||||
### 1. 路径下采样 — 丝滑动画的秘密
|
||||
|
||||
```
|
||||
AMap.Driving 返回原始路径:5000+ 个点(每几米一个)
|
||||
↓ downsamplePath(fullPath, 250)
|
||||
动画路径:250 个点(均匀间隔)
|
||||
↓ setInterval(40ms) → 25fps
|
||||
总时长:250 × 40ms = 10 秒(恒定)
|
||||
```
|
||||
|
||||
**为什么需要下采样?**
|
||||
|
||||
| 不用下采样 | 用下采样 |
|
||||
|------------|----------|
|
||||
| 5000 点 × 40ms = **200 秒**(太慢) | 250 点 × 40ms = **10 秒**(刚好) |
|
||||
| 短距离只有 200 点 = 8 秒(不一致) | 始终 250 点 = **恒定 10 秒** |
|
||||
| 每个点间隔 ~1m,动画抖动 | 均匀间隔,**视觉丝滑** |
|
||||
|
||||
**下采样算法:**
|
||||
|
||||
```ts
|
||||
function downsamplePath(path: [number, number][], target: number): [number, number][] {
|
||||
if (path.length <= target) return [...path]
|
||||
const result: [number, number][] = []
|
||||
const step = (path.length - 1) / (target - 1) // 5000/249 ≈ 20.08
|
||||
for (let i = 0; i < target; i++) {
|
||||
result.push(path[Math.min(Math.round(i * step), path.length - 1)])
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
**为什么选 250?**
|
||||
- 250 × 40ms = 10 秒,不短不长,刚好展示完整轨迹
|
||||
- 25fps 是人眼感知流畅的最低帧率,再低会卡顿
|
||||
- 250 个 `setPosition` 调用对 DOM 压力极小
|
||||
|
||||
### 2. 帧动画引擎
|
||||
|
||||
```ts
|
||||
async function startAnimation(): Promise<void> {
|
||||
// 🔧 容错:护理员不存在 → 自动在起点创建
|
||||
if (!caregiverMarker.value) {
|
||||
if (!originPoint.value) { alert('请先标记起点'); return }
|
||||
await addCaregiver()
|
||||
}
|
||||
|
||||
const total = animPath.value.length - 1 // 249 步
|
||||
|
||||
animTimer = setInterval(() => {
|
||||
if (animStep.value >= total) {
|
||||
stopAnimation() // 到达终点自动停
|
||||
animProgress.value = 1
|
||||
return
|
||||
}
|
||||
animStep.value++
|
||||
animProgress.value = animStep.value / total // 0→1 驱动进度条
|
||||
caregiverMarker.value?.setPosition(animPath.value[animStep.value])
|
||||
}, 40) // 25fps
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| 帧间隔 | 40ms | 25fps,人眼流畅感知阈值 |
|
||||
| 总步数 | 249 | 250 个路径点,首点即起始位置 |
|
||||
| 总时长 | ~10s | 恒定,不受实际距离影响 |
|
||||
| 进度计算 | `step/total` | 0→1,驱动 CSS 进度条宽度 |
|
||||
|
||||
### 3. 三种 status 的完整错误处理
|
||||
|
||||
```ts
|
||||
driving.search(originLngLat, destLngLat, (status, result) => {
|
||||
if (status === 'complete' && result.routes?.length > 0) {
|
||||
// ✅ 成功:提取路径 → 下采样 → 绘制折线
|
||||
} else if (status === 'error') {
|
||||
// ❌ 参数/权限问题:INVALID_USER_SCODE(缺安全密钥)等
|
||||
reject(new Error(`路线规划失败: ${result.info || result.message}`))
|
||||
} else if (status === 'no_data') {
|
||||
// ⚠️ 起终点之间确实无路(如海岛 → 内陆无桥梁)
|
||||
reject(new Error('起终点之间无可用路线'))
|
||||
} else {
|
||||
// ⁉️ 未知状态,不应发生
|
||||
reject(new Error(`路线规划失败 (${status})`))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 4. 点击地图放标记 — 模式切换设计
|
||||
|
||||
```
|
||||
enterSetMode('origin' | 'dest')
|
||||
→ exitSetMode() // 先退出当前模式(互斥)
|
||||
→ settingMode = mode // origin 或 dest
|
||||
→ map.setDefaultCursor('crosshair')
|
||||
→ map.on('click', handler)
|
||||
→ 点击地图:
|
||||
→ 获取经纬度
|
||||
→ mode === 'origin' ? applyOrigin() : applyDestination()
|
||||
→ exitSetMode() // 单次点击后自动退出
|
||||
```
|
||||
|
||||
**三个关键设计决策:**
|
||||
|
||||
| 决策 | 理由 |
|
||||
|------|------|
|
||||
| 单次点击后自动退出 | 避免用户误放多个标记 |
|
||||
| origin 和 dest 互斥 | 同一时间只能设一种点,防止状态混乱 |
|
||||
| 退出时恢复光标 + 解绑事件 | `exitSetMode()` 统一清理,防止事件泄漏 |
|
||||
|
||||
---
|
||||
|
||||
## 考察点汇总
|
||||
|
||||
| 层级 | 考察点 | 难度 |
|
||||
|------|--------|------|
|
||||
| **算法** | 路径下采样(均匀间隔重采样) | ⭐⭐⭐ |
|
||||
| **动画** | setInterval 帧动画 + 进度归一化 | ⭐⭐ |
|
||||
| **API 集成** | AMap.Driving 三种 status 的完整处理 | ⭐⭐⭐ |
|
||||
| **交互设计** | 模式切换 + 点击地图放标记 | ⭐⭐ |
|
||||
| **容错设计** | 护理员缺失时自动创建 | ⭐ |
|
||||
| **资源管理** | setInterval 清理 + 事件解绑 | ⭐⭐ |
|
||||
| **调试** | INVALID_USER_SCODE 问题排查 | ⭐⭐⭐⭐ |
|
||||
|
||||
## 加分项
|
||||
|
||||
- **能解释为什么 250 个点** — 250×40ms=10s 恒定,与距离无关
|
||||
- **能提出变速方案** — 起步加速/到站减速(easeInOutCubic),用动态 interval 替代固定 40ms
|
||||
- **能指出 `new AMap.LngLat(lng, lat)` 的必要性** — 数组 `[lng, lat]` 在某些版本不被 Driving 接受
|
||||
- **能说明 `map: undefined` 会导致 Driving 失败** — Driving 依赖 map 做投影计算
|
||||
- **能分析不同距离下的优化策略** — 短距离提高 target 点数(更细腻),长距离降低(防卡顿)
|
||||
|
||||
## 追问方向
|
||||
|
||||
### 追问 1:如果路线有 10 万个点,250 的下采样会丢失多少细节?
|
||||
|
||||
**期望回答:** 10 万 → 250,每 400 个点取 1 个,城市街道级别的拐弯会被抹平。但视觉上因为标记移动只显示一个点,250 个关键帧足以模拟沿途运动。如果需要更高精度,可以动态调整 target:短距离(<50km)用 500 点,长距离(>500km)用 150 点。
|
||||
|
||||
### 追问 2:为什么用 `setInterval` 而不用 `requestAnimationFrame`?
|
||||
|
||||
**期望回答:**
|
||||
- `rAF` 60fps → 250 点只需 4 秒(太快)
|
||||
- `rAF` 帧率不稳定(后台标签页降到 1fps)→ 动画时间不可预测
|
||||
- `setInterval(40)` 稳定 25fps,后台自动暂停(浏览器节流),回来时从当前位置继续
|
||||
- 需要配合进度条时需要可知的总时长,`setInterval` 更可控
|
||||
|
||||
### 追问 3:`startAnimation` 中为什么要 `await addCaregiver()`?
|
||||
|
||||
**期望回答:** `addCaregiver()` 内部调用 `loadAMap()` 是异步的。如果护理员不存在,需要等标记创建完成再开始动画。`await` 确保 `caregiverMarker.value` 已被赋值,后续 `setInterval` 中才能正常调用 `setPosition`。
|
||||
|
||||
### 追问 4:如何让动画支持「可调速」?
|
||||
|
||||
**期望回答:** 将 40ms 提取为变量 `speed`,暴露给外部。速度加倍 = interval 减半:
|
||||
```ts
|
||||
const speedMap = { '1x': 40, '2x': 20, '4x': 10 }
|
||||
// 或动态计算,进度条相应调整
|
||||
```
|
||||
Reference in New Issue
Block a user