feat(map): 初始视野对到点位聚类中心,并在已授权时开屏静默定位 #16

Merged
zhouruizhe merged 1 commits from zhouruizhe into main 2026-08-04 08:42:55 +08:00
Member

初始视野

  • 新增 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。

初始视野 - 新增 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。
zhouruizhe added 1 commit 2026-08-04 08:23:20 +08:00
初始视野
- 新增 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。
Owner

我已读完 pr.diff、改动涉及的全部源文件(index.vueviewport.tsmap.tsvalidation.tsdataset.ts)、barrel 导出与 location store,并核对了导入解析、旧默认值残留与 uni.getWindowInfo 在本仓库 uni-app 版本(3.0.0-4080720251210001)中的可用性。

🔍 审查结论

严重问题(必须修复)

无。本次改动逻辑自洽,对当前数据集没有破坏正确性的缺陷:

  • centerOnUserLocation 确实会 includePointsRequestVersion += 1src/pages/map/index.vue:463),所以 initializeMap 里那段关于「bump version 能取消已排队的 includeFilteredPoints」的注释(index.vue:616-619)是准确的,静默定位与首帧 includePoints 的竞态被正确处理。
  • isWithinGuangmingArea 用闭区间 >= / <= 替换原 validateCoordinates 的严格 < / > 外判(validation.ts:232-237),边界点判定语义不变,无回归。
  • untouchedDefaultViewport 从「硬编码精确相等」改为「相对 DEFAULT_GUANGMING_VIEWPORT 的阈值比较」(index.vue:160-165),与新 store 初值(map.ts:12-16)一致,且更鲁棒。
  • barrel 已正确再导出:@/domain/poivalidation(含 GUANGMING_POC_BOUNDSisWithinGuangmingArea),@/services/mapviewport(含 computeClusterViewport),导入可解析。
  • fitScale 的墨卡托折算(spanLatitude / cos(centerLat))与瓦片像素公式手推正确;candidates 至少有一个元素(早返回保证),Math.min(...candidates) 安全。

建议改进

  • test/map-viewport.test.ts:506-517 — 用例名叫「装得下全部点位」,但断言只校验中心点落在 POC 包络内、scale ∈ [12,14),并未真正验证「在该 scale 下全部点位都在可视范围内」。而且实际首屏的取景最终由原生 context.includePointsindex.vue:211)再做一次带 padding 的 fit,computeClusterViewport 只决定 includePoints 之前的「中间帧」store 值。建议要么改名(如「聚类中心落在光明区且 scale 为区级」),要么补一个真正的容纳断言:按返回 scale 算出可视经纬跨度,断言每个点到中心的距离都 ≤ 半跨度。

  • src/services/map/viewport.ts:116-120(配合 :140-143computeClusterViewport 的中心取算术中心(centroid),但 fitScale 用的是外接矩形全跨度(max-min)来反推 scale,隐含「centroid 与外接矩形中心重合」的假设。当点位分布偏斜(个别远点)时,centroid 会偏向密集侧,远点一侧的「centroid→边缘」距离会超过半跨度,理论上可能被切到屏外。当前数据集 centroid 与矩形中心只差 0.36 km(注释也提到),加上 fitScale 向下取半级(:147)带来的额外余量,加上首屏还会被 includePoints 覆盖,实际不会出问题;但本 PR 的初衷正是「增删点位后算出来的不会过期」。若想严格保证「一眼看全」,可把 fit 依据从 max-min 改成 2 × max(centroid−min, max−centroid)(取较远的那一半 ×2 作为等效跨度),代价极小。

  • src/pages/map/index.vue:31、605-621launchCenteredOnUser 只在 includeFilteredPoints 里被置回 false:193)。若开屏静默定位已置 true 后地图报错,用户点「重试地图」会再次进入 initializeMap,此时 !launchCenteredOnUser 仍为 false → 跳过 includeFilteredPoints,重试后不会自动取景(markers 仍正常渲染,因为直接绑定 :markers,只是不 fit)。属于较窄的边界路径,影响有限;如需严谨,可在 initializeMap 入口或地图重试动作里重置该标记。

  • src/data/poi/dataset.ts:499-502 vs src/stores/modules/map.ts:12-16 — 两处都是「对齐到聚类中心」的兜底值,但纬度一个是 22.7611、一个是 22.761,相差 1e-4(约 11 m,正好等于 COORDINATE_EPSILON)。功能上无害,但 dataset.ts 的注释声称「与 computeClusterViewport 的结果一致」,两处兜底自身却不一致。建议统一成同一个值(或直接复用 DEFAULT_GUANGMING_VIEWPORT),避免日后迷惑。

  • src/pages/map/index.vue:565-575、639-646restoreLocationOnLaunchonLoad 起步、initializeMaponReady 起步。getSetting + getLocation 多数情况下会在首帧之后才 resolve,于是「已授权用户」开屏可能先看到聚类视野、再跳到自己位置(一次性的居中动画,非抽动回路)。注释表述是「蓝点直接出现在自己位置上」,实际更接近「先聚类、后移到我」。这是可接受的体验,但若想真正做到无缝,需要把首帧 <map> 的初始中心延迟到定位有结果再确定——代价较大,按需取舍。

测试与验证建议

  • 跑单测npm test(本环境未装 node_modulesnpx vitest 拉到 v4 与 vitest.config.ts 不兼容,未能代跑)。重点确认新增的 computeClusterViewport / isWithinGuangmingArea 用例通过,尤其是真实数据集那条 scale ∈ [12,14) 会随点位增删而变脆——增删远距离点位后需同步检视。
  • mp-weixin 真机/开发者工具手动验证(这是本次改动的主要风险面,单测覆盖不到):
    1. 未授权/已拒绝态冷启动:确认不弹微信授权框、也不弹我们的引导框(restoreLocationOnLaunch 仅在 scope.userLocation === true 时才动);视野停在聚类。
    2. 已授权且在光明区内冷启动:蓝点 + 视野落到自己位置(scale 14)。
    3. 已授权但在光明区外(如模拟市民中心坐标):确认把地图甩到无点位区域,视野留在聚类,蓝点在屏外,定位按钮显示「回到我的位置」可作逃生口。
    4. 回归最近三个 commit 修过的「regionchange 回写自激/地图抽动」:开屏居中后再手动拖动、缩放、点标点、回到全域,确认不重新出现抖动。
  • 可选补充单测:针对「静默定位落在区外不居中」加一条 requestCurrentLocation({silent:true}) 行为断言(当前只测了 isWithinGuangmingArea 本身,没测它在 index.vue 里的接线)。
我已读完 `pr.diff`、改动涉及的全部源文件(`index.vue`、`viewport.ts`、`map.ts`、`validation.ts`、`dataset.ts`)、barrel 导出与 location store,并核对了导入解析、旧默认值残留与 `uni.getWindowInfo` 在本仓库 uni-app 版本(`3.0.0-4080720251210001`)中的可用性。 ## 🔍 审查结论 ### 严重问题(必须修复) 无。本次改动逻辑自洽,对当前数据集没有破坏正确性的缺陷: - `centerOnUserLocation` 确实会 `includePointsRequestVersion += 1`(`src/pages/map/index.vue:463`),所以 `initializeMap` 里那段关于「bump version 能取消已排队的 includeFilteredPoints」的注释(`index.vue:616-619`)是准确的,静默定位与首帧 includePoints 的竞态被正确处理。 - `isWithinGuangmingArea` 用闭区间 `>= / <=` 替换原 `validateCoordinates` 的严格 `< / >` 外判(`validation.ts:232-237`),边界点判定语义不变,无回归。 - `untouchedDefaultViewport` 从「硬编码精确相等」改为「相对 `DEFAULT_GUANGMING_VIEWPORT` 的阈值比较」(`index.vue:160-165`),与新 store 初值(`map.ts:12-16`)一致,且更鲁棒。 - barrel 已正确再导出:`@/domain/poi` → `validation`(含 `GUANGMING_POC_BOUNDS`、`isWithinGuangmingArea`),`@/services/map` → `viewport`(含 `computeClusterViewport`),导入可解析。 - `fitScale` 的墨卡托折算(`spanLatitude / cos(centerLat)`)与瓦片像素公式手推正确;`candidates` 至少有一个元素(早返回保证),`Math.min(...candidates)` 安全。 ### 建议改进 - **`test/map-viewport.test.ts:506-517`** — 用例名叫「装得下全部点位」,但断言只校验中心点落在 POC 包络内、`scale ∈ [12,14)`,并未真正验证「在该 scale 下全部点位都在可视范围内」。而且实际首屏的取景最终由原生 `context.includePoints`(`index.vue:211`)再做一次带 padding 的 fit,`computeClusterViewport` 只决定 includePoints 之前的「中间帧」store 值。建议要么改名(如「聚类中心落在光明区且 scale 为区级」),要么补一个真正的容纳断言:按返回 scale 算出可视经纬跨度,断言每个点到中心的距离都 ≤ 半跨度。 - **`src/services/map/viewport.ts:116-120`(配合 `:140-143`)** — `computeClusterViewport` 的中心取算术中心(centroid),但 `fitScale` 用的是外接矩形全跨度(`max-min`)来反推 scale,隐含「centroid 与外接矩形中心重合」的假设。当点位分布偏斜(个别远点)时,centroid 会偏向密集侧,远点一侧的「centroid→边缘」距离会超过半跨度,理论上可能被切到屏外。当前数据集 centroid 与矩形中心只差 0.36 km(注释也提到),加上 `fitScale` 向下取半级(`:147`)带来的额外余量,加上首屏还会被 `includePoints` 覆盖,实际不会出问题;但本 PR 的初衷正是「增删点位后算出来的不会过期」。若想严格保证「一眼看全」,可把 fit 依据从 `max-min` 改成 `2 × max(centroid−min, max−centroid)`(取较远的那一半 ×2 作为等效跨度),代价极小。 - **`src/pages/map/index.vue:31、605-621`** — `launchCenteredOnUser` 只在 `includeFilteredPoints` 里被置回 `false`(`:193`)。若开屏静默定位已置 `true` 后地图报错,用户点「重试地图」会再次进入 `initializeMap`,此时 `!launchCenteredOnUser` 仍为 false → 跳过 `includeFilteredPoints`,重试后不会自动取景(markers 仍正常渲染,因为直接绑定 `:markers`,只是不 fit)。属于较窄的边界路径,影响有限;如需严谨,可在 `initializeMap` 入口或地图重试动作里重置该标记。 - **`src/data/poi/dataset.ts:499-502` vs `src/stores/modules/map.ts:12-16`** — 两处都是「对齐到聚类中心」的兜底值,但纬度一个是 `22.7611`、一个是 `22.761`,相差 `1e-4`(约 11 m,正好等于 `COORDINATE_EPSILON`)。功能上无害,但 dataset.ts 的注释声称「与 computeClusterViewport 的结果一致」,两处兜底自身却不一致。建议统一成同一个值(或直接复用 `DEFAULT_GUANGMING_VIEWPORT`),避免日后迷惑。 - **`src/pages/map/index.vue:565-575、639-646`** — `restoreLocationOnLaunch` 在 `onLoad` 起步、`initializeMap` 在 `onReady` 起步。`getSetting + getLocation` 多数情况下会在首帧之后才 resolve,于是「已授权用户」开屏可能先看到聚类视野、再跳到自己位置(一次性的居中动画,非抽动回路)。注释表述是「蓝点直接出现在自己位置上」,实际更接近「先聚类、后移到我」。这是可接受的体验,但若想真正做到无缝,需要把首帧 `<map>` 的初始中心延迟到定位有结果再确定——代价较大,按需取舍。 ### 测试与验证建议 - **跑单测**:`npm test`(本环境未装 `node_modules`,`npx vitest` 拉到 v4 与 `vitest.config.ts` 不兼容,未能代跑)。重点确认新增的 `computeClusterViewport` / `isWithinGuangmingArea` 用例通过,尤其是真实数据集那条 `scale ∈ [12,14)` 会随点位增删而变脆——增删远距离点位后需同步检视。 - **mp-weixin 真机/开发者工具手动验证**(这是本次改动的主要风险面,单测覆盖不到): 1. 未授权/已拒绝态冷启动:确认**不弹**微信授权框、也不弹我们的引导框(`restoreLocationOnLaunch` 仅在 `scope.userLocation === true` 时才动);视野停在聚类。 2. 已授权且在光明区内冷启动:蓝点 + 视野落到自己位置(scale 14)。 3. 已授权但在光明区外(如模拟市民中心坐标):确认**不**把地图甩到无点位区域,视野留在聚类,蓝点在屏外,定位按钮显示「回到我的位置」可作逃生口。 4. 回归最近三个 commit 修过的「regionchange 回写自激/地图抽动」:开屏居中后再手动拖动、缩放、点标点、回到全域,确认不重新出现抖动。 - **可选补充单测**:针对「静默定位落在区外不居中」加一条 `requestCurrentLocation({silent:true})` 行为断言(当前只测了 `isWithinGuangmingArea` 本身,没测它在 `index.vue` 里的接线)。
zhouruizhe merged commit fed2a8bf7b into main 2026-08-04 08:42:55 +08:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: team/gmTouringMiniApp#16