修正设备定位坐标系归一化
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
51
talkingq-url/tests/test_location_coordinate_normalization.py
Normal file
51
talkingq-url/tests/test_location_coordinate_normalization.py
Normal 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 == []
|
||||||
Reference in New Issue
Block a user