fix: 修正 Cesium 巡航车道中心对齐

This commit is contained in:
2026-08-08 15:21:49 +08:00
parent aeb2cec021
commit 5658e7337d
17 changed files with 1081 additions and 192 deletions

View File

@@ -149,23 +149,55 @@ def parse_height(feature_tags, default):
class Projector:
"""Equirectangular projection about the centre of the OSM bounds.
"""WGS84 ECEF to local ENU projection about the OSM bounds centre.
Output is metres in a local ENU frame (X east, Y north), which is what both
the Blender scene and the Cesium GLB are authored in.
the Blender scene and the Cesium GLB are authored in. Cesium places the
GLB with eastNorthUpToFixedFrame, so using the same ellipsoid transform is
required to keep route coordinates aligned across the whole scene.
"""
WGS84_A = 6378137.0
WGS84_E2 = 6.6943799901413165e-3
def __init__(self, bounds):
self.bounds = bounds
self.lon0 = (bounds["min_lon"] + bounds["max_lon"]) / 2
self.lat0 = (bounds["min_lat"] + bounds["max_lat"]) / 2
self.m_per_lat = 111320.0
self.m_per_lon = 111320.0 * math.cos(math.radians(self.lat0))
self._lon0_rad = math.radians(self.lon0)
self._lat0_rad = math.radians(self.lat0)
self._sin_lon0 = math.sin(self._lon0_rad)
self._cos_lon0 = math.cos(self._lon0_rad)
self._sin_lat0 = math.sin(self._lat0_rad)
self._cos_lat0 = math.cos(self._lat0_rad)
self._origin_ecef = self._ecef(self._lon0_rad, self._lat0_rad)
denominator = math.sqrt(1.0 - self.WGS84_E2 * self._sin_lat0 ** 2)
prime_vertical_radius = self.WGS84_A / denominator
meridional_radius = self.WGS84_A * (1.0 - self.WGS84_E2) / denominator ** 3
radians_per_degree = math.pi / 180.0
self.m_per_lon = prime_vertical_radius * self._cos_lat0 * radians_per_degree
self.m_per_lat = meridional_radius * radians_per_degree
def xy(self, lon_lat):
lon, lat = lon_lat
return ((lon - self.lon0) * self.m_per_lon,
(lat - self.lat0) * self.m_per_lat)
x, y, z = self._ecef(math.radians(lon), math.radians(lat))
dx = x - self._origin_ecef[0]
dy = y - self._origin_ecef[1]
dz = z - self._origin_ecef[2]
east = -self._sin_lon0 * dx + self._cos_lon0 * dy
north = (-self._sin_lat0 * self._cos_lon0 * dx
- self._sin_lat0 * self._sin_lon0 * dy
+ self._cos_lat0 * dz)
return east, north
def _ecef(self, lon_rad, lat_rad):
sin_lat = math.sin(lat_rad)
cos_lat = math.cos(lat_rad)
radius = self.WGS84_A / math.sqrt(1.0 - self.WGS84_E2 * sin_lat ** 2)
return (radius * cos_lat * math.cos(lon_rad),
radius * cos_lat * math.sin(lon_rad),
radius * (1.0 - self.WGS84_E2) * sin_lat)
def inside(self, lon_lat, pad=0.00035):
lon, lat = lon_lat

View File

@@ -238,14 +238,23 @@ class ProjectorTest(unittest.TestCase):
self.assertGreater(east, 0.0)
self.assertGreater(north, 0.0)
def test_longitude_metres_shrink_with_latitude(self):
self.assertAlmostEqual(
self.projector.m_per_lon,
111320.0 * math.cos(math.radians(30.005)),
places=6,
)
def test_wgs84_local_scale_matches_ellipsoid(self):
latitude = math.radians(30.005)
denominator = math.sqrt(1.0 - Projector.WGS84_E2 * math.sin(latitude) ** 2)
expected_lon = (Projector.WGS84_A / denominator
* math.cos(latitude) * math.pi / 180.0)
expected_lat = (Projector.WGS84_A * (1.0 - Projector.WGS84_E2)
/ denominator ** 3 * math.pi / 180.0)
self.assertAlmostEqual(self.projector.m_per_lon, expected_lon, places=6)
self.assertAlmostEqual(self.projector.m_per_lat, expected_lat, places=6)
self.assertLess(self.projector.m_per_lon, self.projector.m_per_lat)
def test_projection_matches_local_wgs84_scale(self):
east, _ = self.projector.xy((114.006, 30.005))
_, north = self.projector.xy((114.005, 30.006))
self.assertAlmostEqual(east, self.projector.m_per_lon * 0.001, places=4)
self.assertAlmostEqual(north, self.projector.m_per_lat * 0.001, places=4)
def test_inside_honours_the_pad(self):
self.assertTrue(self.projector.inside((114.005, 30.005)))
# Default pad is 0.00035 degrees, so just outside the box still counts.