docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖: - 逐文件可移植性审计(核心逻辑层约 90% 可直接复用) - 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC + ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道 - 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel) - 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico) - macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils 放进 excludes、AlreadyMountedError 当成功处理 - 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等) - 验收基线(复用现有测试套件)与建议的工作顺序
This commit is contained in:
commit
85e9a53750
65 files changed
+18603
No files matched your search
@@ -0,0 +1,234 @@
|
||||
"""地理计算:距离、方位角、弧长插值、路径加密。
|
||||
|
||||
约定:坐标一律 (lat, lng) 的浮点元组,单位为度,WGS84。
|
||||
对外 JSON 里的坐标是 [lng, lat](GeoJSON 习惯),转换在 route_engine 里做。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from .protocol import EARTH_RADIUS_M, clamp, round6
|
||||
|
||||
|
||||
def haversine(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
|
||||
"""两点大圆距离(米)。"""
|
||||
p1 = math.radians(lat1)
|
||||
p2 = math.radians(lat2)
|
||||
dp = p2 - p1
|
||||
dl = math.radians(lng2 - lng1)
|
||||
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
|
||||
return 2 * EARTH_RADIUS_M * math.asin(min(1.0, math.sqrt(a)))
|
||||
|
||||
|
||||
def bearing(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
|
||||
"""起点到终点的方位角(度,0=正北,顺时针)。"""
|
||||
p1 = math.radians(lat1)
|
||||
p2 = math.radians(lat2)
|
||||
dl = math.radians(lng2 - lng1)
|
||||
y = math.sin(dl) * math.cos(p2)
|
||||
x = math.cos(p1) * math.sin(p2) - math.sin(p1) * math.cos(p2) * math.cos(dl)
|
||||
return (math.degrees(math.atan2(y, x)) + 360.0) % 360.0
|
||||
|
||||
|
||||
def lerp(a: float, b: float, t: float) -> float:
|
||||
return a + (b - a) * t
|
||||
|
||||
|
||||
def lerp_point(p1: tuple[float, float], p2: tuple[float, float], t: float) -> tuple[float, float]:
|
||||
"""线性插值(小范围下与测地线插值差异可忽略)。"""
|
||||
return (lerp(p1[0], p2[0], t), lerp(p1[1], p2[1], t))
|
||||
|
||||
|
||||
def catmull_rom(
|
||||
p0: tuple[float, float],
|
||||
p1: tuple[float, float],
|
||||
p2: tuple[float, float],
|
||||
p3: tuple[float, float],
|
||||
t: float,
|
||||
) -> tuple[float, float]:
|
||||
"""Catmull-Rom 样条插值,用于把折线平滑成车道路径。"""
|
||||
t2 = t * t
|
||||
t3 = t2 * t
|
||||
lat = 0.5 * (
|
||||
(2 * p1[0])
|
||||
+ (-p0[0] + p2[0]) * t
|
||||
+ (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2
|
||||
+ (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3
|
||||
)
|
||||
lng = 0.5 * (
|
||||
(2 * p1[1])
|
||||
+ (-p0[1] + p2[1]) * t
|
||||
+ (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2
|
||||
+ (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3
|
||||
)
|
||||
return (lat, lng)
|
||||
|
||||
|
||||
def cumulative_lengths(path: list[tuple[float, float]]) -> list[float]:
|
||||
"""返回与 path 等长的累计距离数组(米),首元素为 0。"""
|
||||
acc = [0.0]
|
||||
total = 0.0
|
||||
for i in range(1, len(path)):
|
||||
total += haversine(path[i - 1][0], path[i - 1][1], path[i][0], path[i][1])
|
||||
acc.append(total)
|
||||
return acc
|
||||
|
||||
|
||||
def _binary_search(acc: list[float], value: float) -> int:
|
||||
"""返回 i 使得 acc[i] <= value < acc[i+1](value 超界时返回最后一个区间)。"""
|
||||
lo, hi = 0, len(acc) - 1
|
||||
if value <= acc[0]:
|
||||
return 0
|
||||
if value >= acc[-1]:
|
||||
return max(0, hi - 1)
|
||||
while lo < hi - 1:
|
||||
mid = (lo + hi) // 2
|
||||
if acc[mid] <= value:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
return lo
|
||||
|
||||
|
||||
def interpolate(
|
||||
path: list[tuple[float, float]], acc: list[float], distance: float
|
||||
) -> tuple[tuple[float, float], float]:
|
||||
"""按弧长在路径上取点,返回 ((lat,lng), 该段内的比例 t)。"""
|
||||
if not path:
|
||||
raise ValueError("路径为空")
|
||||
if len(path) == 1:
|
||||
return path[0], 0.0
|
||||
i = _binary_search(acc, distance)
|
||||
seg = acc[i + 1] - acc[i]
|
||||
t = 0.0 if seg <= 1e-9 else clamp((distance - acc[i]) / seg, 0.0, 1.0)
|
||||
return lerp_point(path[i], path[i + 1], t), t
|
||||
|
||||
|
||||
def slice_path(
|
||||
path: list[tuple[float, float]], acc: list[float], distance: float
|
||||
) -> list[tuple[float, float]]:
|
||||
"""返回从起点到指定弧长的子路径(用于前端高亮已播放段)。"""
|
||||
if len(path) < 2:
|
||||
return list(path)
|
||||
i = _binary_search(acc, distance)
|
||||
out = path[: i + 1]
|
||||
point, _ = interpolate(path, acc, distance)
|
||||
out.append(point)
|
||||
return out
|
||||
|
||||
|
||||
def densify_segment(
|
||||
p1: tuple[float, float], p2: tuple[float, float], step_m: float
|
||||
) -> list[tuple[float, float]]:
|
||||
"""在两点之间按 step_m 加密(不含起点,含终点)。"""
|
||||
dist = haversine(p1[0], p1[1], p2[0], p2[1])
|
||||
if dist <= 1e-6:
|
||||
return [p2]
|
||||
n = max(1, int(math.ceil(dist / max(1.0, step_m))))
|
||||
return [lerp_point(p1, p2, k / n) for k in range(1, n + 1)]
|
||||
|
||||
|
||||
def densify(
|
||||
points: list[tuple[float, float]], step_m: float = 2.0, smooth: bool = False
|
||||
) -> list[tuple[float, float]]:
|
||||
"""把稀疏折线加密到约 step_m 的采样间隔;smooth=True 时用样条平滑。"""
|
||||
pts = _dedupe(points)
|
||||
if len(pts) < 2:
|
||||
return pts
|
||||
if not smooth or len(pts) < 3:
|
||||
out = [pts[0]]
|
||||
for i in range(1, len(pts)):
|
||||
out.extend(densify_segment(pts[i - 1], pts[i], step_m))
|
||||
return out
|
||||
|
||||
padded = [pts[0]] + pts + [pts[-1]]
|
||||
out = [pts[0]]
|
||||
for i in range(1, len(padded) - 2):
|
||||
p0, p1, p2, p3 = padded[i - 1], padded[i], padded[i + 1], padded[i + 2]
|
||||
seg_len = haversine(p1[0], p1[1], p2[0], p2[1])
|
||||
n = max(1, int(math.ceil(seg_len / max(1.0, step_m))))
|
||||
for k in range(1, n + 1):
|
||||
out.append(catmull_rom(p0, p1, p2, p3, k / n))
|
||||
return out
|
||||
|
||||
|
||||
def _dedupe(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
out: list[tuple[float, float]] = []
|
||||
for point in points:
|
||||
if not out or haversine(out[-1][0], out[-1][1], point[0], point[1]) > 0.05:
|
||||
out.append(point)
|
||||
return out
|
||||
|
||||
|
||||
def simplify(points: list[tuple[float, float]], tolerance_m: float = 1.5) -> list[tuple[float, float]]:
|
||||
"""Douglas-Peucker 简化,用于压缩轨迹体积。"""
|
||||
if len(points) < 3:
|
||||
return list(points)
|
||||
keep = [False] * len(points)
|
||||
keep[0] = keep[-1] = True
|
||||
stack = [(0, len(points) - 1)]
|
||||
while stack:
|
||||
start, end = stack.pop()
|
||||
if end <= start + 1:
|
||||
continue
|
||||
max_dist = -1.0
|
||||
index = start
|
||||
for i in range(start + 1, end):
|
||||
d = _point_segment_distance(points[i], points[start], points[end])
|
||||
if d > max_dist:
|
||||
max_dist = d
|
||||
index = i
|
||||
if max_dist > tolerance_m:
|
||||
keep[index] = True
|
||||
stack.append((start, index))
|
||||
stack.append((index, end))
|
||||
return [p for p, k in zip(points, keep) if k]
|
||||
|
||||
|
||||
def _point_segment_distance(
|
||||
p: tuple[float, float], a: tuple[float, float], b: tuple[float, float]
|
||||
) -> float:
|
||||
"""点到线段的近似距离(米):先把经纬度投影到局部平面。"""
|
||||
lat0 = math.radians((a[0] + b[0]) / 2)
|
||||
scale_x = EARTH_RADIUS_M * math.cos(lat0) * math.pi / 180.0
|
||||
scale_y = EARTH_RADIUS_M * math.pi / 180.0
|
||||
px, py = (p[1] - a[1]) * scale_x, (p[0] - a[0]) * scale_y
|
||||
bx, by = (b[1] - a[1]) * scale_x, (b[0] - a[0]) * scale_y
|
||||
denom = bx * bx + by * by
|
||||
if denom <= 1e-12:
|
||||
return math.hypot(px, py)
|
||||
t = clamp((px * bx + py * by) / denom, 0.0, 1.0)
|
||||
return math.hypot(px - t * bx, py - t * by)
|
||||
|
||||
|
||||
def wgs84_to_gcj02_approx(lat: float, lng: float) -> tuple[float, float]:
|
||||
"""后端不参与地图显示,这里仅提供与前端一致的近似实现(用于自检)。"""
|
||||
if lng < 72.004 or lng > 137.8347 or lat < 0.8293 or lat > 55.8271:
|
||||
return lat, lng
|
||||
a = 6378245.0
|
||||
ee = 0.00669342162296594323
|
||||
dlat = _transform_lat(lng - 105.0, lat - 35.0)
|
||||
dlng = _transform_lng(lng - 105.0, lat - 35.0)
|
||||
rad_lat = lat / 180.0 * math.pi
|
||||
magic = math.sin(rad_lat)
|
||||
magic = 1 - ee * magic * magic
|
||||
sqrt_magic = math.sqrt(magic)
|
||||
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * math.pi)
|
||||
dlng = (dlng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * math.pi)
|
||||
return round6(lat + dlat), round6(lng + dlng)
|
||||
|
||||
|
||||
def _transform_lat(x: float, y: float) -> float:
|
||||
ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x))
|
||||
ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(y * math.pi) + 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0
|
||||
ret += (160.0 * math.sin(y / 12.0 * math.pi) + 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
|
||||
|
||||
def _transform_lng(x: float, y: float) -> float:
|
||||
ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x))
|
||||
ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(x * math.pi) + 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0
|
||||
ret += (150.0 * math.sin(x / 12.0 * math.pi) + 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0
|
||||
return ret
|
||||
Reference in new issue
Block a user