修正设备定位坐标系归一化

This commit is contained in:
stu2not
2026-06-29 15:50:48 +08:00
parent a2b873a3c2
commit cc67e8877a
2 changed files with 121 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
import math
from typing import Any from typing import Any
from services.database_service_base import DatabaseServiceBase from services.database_service_base import DatabaseServiceBase
@@ -23,6 +24,10 @@ ADDRESS_RESOLVE_PENDING = 0
ADDRESS_RESOLVE_SUCCESS = 1 ADDRESS_RESOLVE_SUCCESS = 1
ADDRESS_RESOLVE_FAILED = 2 ADDRESS_RESOLVE_FAILED = 2
INVALID_COORDINATE_EPSILON = 0.0000001 INVALID_COORDINATE_EPSILON = 0.0000001
GCJ02_COORD_TYPE = "gcj02"
WGS84_COORD_TYPES = {"", "wgs84", "gps"}
WGS84_TO_GCJ02_A = 6378245.0
WGS84_TO_GCJ02_EE = 0.00669342162296594323
def is_valid_coordinate_pair(latitude: float | None, longitude: float | None) -> bool: def is_valid_coordinate_pair(latitude: float | None, longitude: float | None) -> bool:
@@ -38,6 +43,62 @@ def is_valid_coordinate_pair(latitude: float | None, longitude: float | None) ->
return not (abs(lat) < INVALID_COORDINATE_EPSILON and abs(lng) < INVALID_COORDINATE_EPSILON) return not (abs(lat) < INVALID_COORDINATE_EPSILON and abs(lng) < INVALID_COORDINATE_EPSILON)
def normalize_location_coordinate(
*,
latitude: float,
longitude: float,
coord_type: str | None,
) -> tuple[float, float, str]:
normalized_type = str(coord_type or "").strip().lower()
if normalized_type == GCJ02_COORD_TYPE:
return float(latitude), float(longitude), GCJ02_COORD_TYPE
if normalized_type in WGS84_COORD_TYPES:
lng, lat = wgs84_to_gcj02(float(longitude), float(latitude))
return lat, lng, GCJ02_COORD_TYPE
return float(latitude), float(longitude), normalized_type
def wgs84_to_gcj02(longitude: float, latitude: float) -> tuple[float, float]:
if _is_out_of_china(longitude, latitude):
return longitude, latitude
dlat = _transform_lat(longitude - 105.0, latitude - 35.0)
dlng = _transform_lng(longitude - 105.0, latitude - 35.0)
radlat = latitude / 180.0 * math.pi
magic = math.sin(radlat)
magic = 1 - WGS84_TO_GCJ02_EE * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / (
(WGS84_TO_GCJ02_A * (1 - WGS84_TO_GCJ02_EE)) / (magic * sqrtmagic) * math.pi
)
dlng = (dlng * 180.0) / (
WGS84_TO_GCJ02_A / sqrtmagic * math.cos(radlat) * math.pi
)
return longitude + dlng, latitude + dlat
def _is_out_of_china(longitude: float, latitude: float) -> bool:
return not (72.004 <= longitude <= 137.8347 and 0.8293 <= latitude <= 55.8271)
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
ret += 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
ret += 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
class LocationService(DatabaseServiceBase): class LocationService(DatabaseServiceBase):
def __init__(self): def __init__(self):
super().__init__(service_name="location_service") super().__init__(service_name="location_service")
@@ -176,10 +237,16 @@ class LocationService(DatabaseServiceBase):
if not binding_row or binding_row["child_id"] is None: if not binding_row or binding_row["child_id"] is None:
return None return None
normalized_lat, normalized_lng, normalized_coord_type = normalize_location_coordinate(
latitude=float(latitude),
longitude=float(longitude),
coord_type=coord_type,
)
payload = _MQTTLocationPayload( payload = _MQTTLocationPayload(
coord_type=(coord_type or "gcj02").strip() or "gcj02", coord_type=normalized_coord_type,
lat=float(latitude), lat=normalized_lat,
lng=float(longitude), lng=normalized_lng,
accuracy_m=accuracy_m, accuracy_m=accuracy_m,
altitude_m=altitude_m, altitude_m=altitude_m,
speed_mps=speed_mps, speed_mps=speed_mps,

View File

@@ -0,0 +1,51 @@
import pytest
from banban.service.location import (
LocationService,
normalize_location_coordinate,
)
def test_normalize_default_device_location_from_wgs84_to_gcj02():
lat, lng, coord_type = normalize_location_coordinate(
latitude=30.223122166666666,
longitude=120.25837883333334,
coord_type=None,
)
assert coord_type == "gcj02"
assert lat == pytest.approx(30.22066658053894)
assert lng == pytest.approx(120.26284572518867)
def test_normalize_explicit_gcj02_does_not_convert_again():
lat, lng, coord_type = normalize_location_coordinate(
latitude=30.223122166666666,
longitude=120.25837883333334,
coord_type="gcj02",
)
assert coord_type == "gcj02"
assert lat == pytest.approx(30.223122166666666)
assert lng == pytest.approx(120.25837883333334)
@pytest.mark.asyncio
async def test_mqtt_location_report_ignores_zero_zero_before_normalization(monkeypatch):
service = LocationService()
sessions = []
async def fail_get_session():
sessions.append("called")
raise AssertionError("invalid location should not open a database session")
monkeypatch.setattr(service, "get_session", fail_get_session)
row = await service.report_mqtt_device_location(
device_id="TalkingQ_device001",
latitude=0,
longitude=0,
)
assert row is None
assert sessions == []