feat(map): 初始视野对到点位聚类中心,并在已授权时开屏静默定位
初始视野 - 新增 computeClusterViewport:把全部点位当一个聚类,中心取算术中心 (跟点位密度走,不像外接矩形中心那样被个别远点拽偏),scale 由聚类跨度 反推出「一眼看全」的档位。当前 30 个点位算出 (113.92754, 22.76105) scale 12 —— 原来的 scale 11 会把光明区缩成一小块,留一圈空白。 - 视野改成运行时实时算,不再读数据集里手写的 defaultViewport:增删点位之后 手写值会过期,算出来的不会。手写值降级为点位为空时的兜底。 - 「回到全域」用同一个聚类视野。 - loadDataset 里判断「视野还没被用户动过」原来是硬编码 113.935/22.748/11 三个字面量,跟 DEFAULT_GUANGMING_VIEWPORT 重复;改成直接跟常量比。 开屏定位 - 新增 restoreLocationOnLaunch:仅当 scope.userLocation 已授权时静默定位并 居中,蓝点直接出现在用户位置上。未决定/已拒绝一律不碰,避免小程序第一帧 就弹微信授权框、拒绝后再连弹一个引导框;那两种状态留给定位按钮。 - 静默定位只在用户确实在光明区包络内才居中,否则把地图甩到没有任何点位的 地方比停在聚类视野更糟。 - 静默失败不弹提示,但仍写入终态,否则 status 卡在 locating、定位按钮 永远显示「定位中…」。 - 新增 isWithinGuangmingArea,与数据校验共用同一个包络定义。 需要说明:地图漂到几内亚湾不是「开屏没请求定位」造成的,而是没有 fix 时 (0, 0) 被写进 viewport。堵住它的是 isTrustworthyCoordinate(7ef4094), 开屏定位是叠在守卫之上的体验改进,不是替代 —— 用户拒权、模拟器没设位置、 室内超时都还是拿不到 fix。
This commit is contained in:
+103
-2
@@ -1,6 +1,8 @@
|
||||
import type { MapViewport } from '@/domain/poi'
|
||||
import type { GeoPoint, MapViewport } from '@/domain/poi'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isSignificantViewportChange } from '@/services/map'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import { GUANGMING_POC_BOUNDS, isWithinGuangmingArea } from '@/domain/poi'
|
||||
import { computeClusterViewport, isSignificantViewportChange } from '@/services/map'
|
||||
|
||||
const current: MapViewport = { longitude: 113.935, latitude: 22.748, scale: 13 }
|
||||
|
||||
@@ -31,3 +33,102 @@ describe('isSignificantViewportChange', () => {
|
||||
expect(isSignificantViewportChange(current, next)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
const VIEWPORT_SIZE = { width: 375, height: 560 }
|
||||
|
||||
describe('computeClusterViewport', () => {
|
||||
it('没有可用点位时返回 null', () => {
|
||||
expect(computeClusterViewport([])).toBeNull()
|
||||
// (0, 0) 是定位没拿到 fix 时的产物,不该参与聚类。
|
||||
expect(computeClusterViewport([{ longitude: 0, latitude: 0 }])).toBeNull()
|
||||
expect(computeClusterViewport([{ longitude: Number.NaN, latitude: 22.7 }])).toBeNull()
|
||||
})
|
||||
|
||||
it('中心点取算术中心,密度大的一侧更靠中间', () => {
|
||||
// 三个点挤在西侧,一个点在东侧远处:算术中心应偏西,而不是落在东西正中。
|
||||
const points: GeoPoint[] = [
|
||||
{ longitude: 113.90, latitude: 22.75 },
|
||||
{ longitude: 113.91, latitude: 22.75 },
|
||||
{ longitude: 113.92, latitude: 22.75 },
|
||||
{ longitude: 114.00, latitude: 22.75 },
|
||||
]
|
||||
const viewport = computeClusterViewport(points, VIEWPORT_SIZE)!
|
||||
|
||||
expect(viewport.longitude).toBeCloseTo(113.9325, 4)
|
||||
// 外接矩形中心是 113.95,算术中心必须比它更靠西。
|
||||
expect(viewport.longitude).toBeLessThan(113.95)
|
||||
})
|
||||
|
||||
it('跨度越大 scale 越小,且始终落在微信允许的 3~20', () => {
|
||||
const tight = computeClusterViewport([
|
||||
{ longitude: 113.93, latitude: 22.75 },
|
||||
{ longitude: 113.94, latitude: 22.76 },
|
||||
], VIEWPORT_SIZE)!
|
||||
const wide = computeClusterViewport([
|
||||
{ longitude: 113.50, latitude: 22.40 },
|
||||
{ longitude: 114.40, latitude: 23.10 },
|
||||
], VIEWPORT_SIZE)!
|
||||
|
||||
expect(tight.scale).toBeGreaterThan(wide.scale)
|
||||
for (const scale of [tight.scale, wide.scale]) {
|
||||
expect(scale).toBeGreaterThanOrEqual(3)
|
||||
expect(scale).toBeLessThanOrEqual(20)
|
||||
}
|
||||
})
|
||||
|
||||
it('所有点位重合时给街区级视野', () => {
|
||||
const viewport = computeClusterViewport([
|
||||
{ longitude: 113.93, latitude: 22.75 },
|
||||
{ longitude: 113.93, latitude: 22.75 },
|
||||
], VIEWPORT_SIZE)!
|
||||
|
||||
expect(viewport).toEqual({ longitude: 113.93, latitude: 22.75, scale: 15 })
|
||||
})
|
||||
|
||||
it('屏幕越窄 scale 越小', () => {
|
||||
const points: GeoPoint[] = [
|
||||
{ longitude: 113.88, latitude: 22.70 },
|
||||
{ longitude: 113.97, latitude: 22.81 },
|
||||
]
|
||||
const narrow = computeClusterViewport(points, { width: 320, height: 480 })!
|
||||
const wide = computeClusterViewport(points, { width: 768, height: 900 })!
|
||||
|
||||
expect(narrow.scale).toBeLessThanOrEqual(wide.scale)
|
||||
})
|
||||
|
||||
it('真实数据集的聚类视野落在光明区范围内,且装得下全部点位', () => {
|
||||
const pois = getPoiRepository().getPoiSummaries()
|
||||
const viewport = computeClusterViewport(pois, VIEWPORT_SIZE)!
|
||||
|
||||
expect(viewport.longitude).toBeGreaterThanOrEqual(GUANGMING_POC_BOUNDS.minLongitude)
|
||||
expect(viewport.longitude).toBeLessThanOrEqual(GUANGMING_POC_BOUNDS.maxLongitude)
|
||||
expect(viewport.latitude).toBeGreaterThanOrEqual(GUANGMING_POC_BOUNDS.minLatitude)
|
||||
expect(viewport.latitude).toBeLessThanOrEqual(GUANGMING_POC_BOUNDS.maxLatitude)
|
||||
// 区级取景:11 级会把光明区缩成一小块,14 级又装不下南北 12.6 km 的跨度。
|
||||
expect(viewport.scale).toBeGreaterThanOrEqual(12)
|
||||
expect(viewport.scale).toBeLessThan(14)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isWithinGuangmingArea', () => {
|
||||
it('本期全部点位都落在包络内', () => {
|
||||
for (const poi of getPoiRepository().getPoiSummaries())
|
||||
expect(isWithinGuangmingArea(poi.longitude, poi.latitude)).toBe(true)
|
||||
})
|
||||
|
||||
it('区外坐标返回 false,用于决定开屏是否把视野挪到用户身上', () => {
|
||||
// 深圳市民中心、广州塔:都是真实可达的位置,但画面里看不到本期点位。
|
||||
expect(isWithinGuangmingArea(114.0655, 22.5477)).toBe(false)
|
||||
expect(isWithinGuangmingArea(113.3245, 23.1066)).toBe(false)
|
||||
// (0, 0) 落在几内亚湾,同样在包络外。
|
||||
expect(isWithinGuangmingArea(0, 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('包络边界闭区间', () => {
|
||||
const { minLongitude, maxLongitude, minLatitude, maxLatitude } = GUANGMING_POC_BOUNDS
|
||||
expect(isWithinGuangmingArea(minLongitude, minLatitude)).toBe(true)
|
||||
expect(isWithinGuangmingArea(maxLongitude, maxLatitude)).toBe(true)
|
||||
expect(isWithinGuangmingArea(minLongitude - 0.001, minLatitude)).toBe(false)
|
||||
expect(isWithinGuangmingArea(maxLongitude, maxLatitude + 0.001)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user