Repository navigation
Expand file tree
/
Copy pathtransformer.py
More file actions
260 lines (212 loc) · 9.44 KB
/
Copy pathtransformer.py
File metadata and controls
260 lines (212 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
"""Unified coordinate validation, unit conversion, and exception-to-warning adapter.
Centralizes API-level validation and transformation dispatch across Legacy (/cooOpService)
and Modern (/api/v1/transform/*) endpoints while delegating geodetic math and domain
checks to pytransdatro.TransRO.
"""
import math
from typing import Any, Dict, List, Optional, Sequence, Tuple
import pytransdatro.exceptions as exc
from api.schemas.point import AngleUnitEnum, OperationEnum, PointResponse
from pytransdatro.trans_ro import TransRO
LEGACY_SUPPORTED_OPS = {
"Stereo70ToETRS89",
"ETRS89ToStereo70",
"Stereo30ToETRS89",
"ETRS89ToStereo30",
}
STEREO30_WARNING = "Stereo30 transformation is obsolete and unsupported"
WARNING_WRONG_DIMENSION = "Wrong dimension"
WARNING_INVALID_COORDINATE = "Invalid coordinate data"
WARNING_OUT_OF_GRID = "Out of grid"
WARNING_NO_DATA = "No data on grid"
def validate_legacy_operation(coo_op: str) -> None:
"""Validates that the legacy operation name is recognized.
:param coo_op: Operation identifier string
:type coo_op: str
:raises ValueError: If coo_op is not a defined legacy transformation
"""
if coo_op not in LEGACY_SUPPORTED_OPS:
raise ValueError(f"There is no defined transformation with name {coo_op}")
def parse_legacy_semicolon_coos(coos_str: str) -> Tuple[List[float], Optional[str]]:
"""Parses a semicolon-delimited coordinate string replicating Java String.split(';') semantics.
Trailing empty segments (e.g. ``500000;500000;``) are stripped, whereas leading or
intermediate empty segments (e.g. ``;500000`` or ``500000;;500000``) are rejected as
invalid coordinate data.
:param coos_str: Raw semicolon-separated coordinate query string
:type coos_str: str
:return: Tuple of ``(parsed_floats, warning_or_none)``
:rtype: Tuple[List[float], Optional[str]]
"""
raw_tokens = coos_str.split(";")
while raw_tokens and raw_tokens[-1].strip() == "":
raw_tokens.pop()
if not raw_tokens:
return [], WARNING_INVALID_COORDINATE
parsed_coords: List[float] = []
for token in raw_tokens:
stripped = token.strip()
if not stripped:
return [], WARNING_INVALID_COORDINATE
try:
val = float(stripped)
except ValueError:
return [], WARNING_INVALID_COORDINATE
if not math.isfinite(val):
return [], WARNING_INVALID_COORDINATE
parsed_coords.append(val)
return parsed_coords, None
def parse_numeric_coord_list(raw_coords: Any) -> Tuple[List[float], Optional[str]]:
"""Validates and converts a sequence of raw coordinate values into finite floats.
:param raw_coords: Input sequence of coordinate values
:type raw_coords: Any
:return: Tuple of ``(numeric_coords, warning_or_none)``
:rtype: Tuple[List[float], Optional[str]]
"""
if not isinstance(raw_coords, (list, tuple)):
return [], WARNING_INVALID_COORDINATE
numeric_coords: List[float] = []
for c in raw_coords:
if isinstance(c, bool) or c is None:
return [], WARNING_INVALID_COORDINATE
try:
val = float(c)
except (ValueError, TypeError):
return [], WARNING_INVALID_COORDINATE
if not math.isfinite(val):
return [], WARNING_INVALID_COORDINATE
numeric_coords.append(val)
return numeric_coords, None
def execute_point_transformation(
trans: TransRO,
op: str,
coords: Sequence[float],
unit: AngleUnitEnum = AngleUnitEnum.RADIANS,
source: int = 1,
) -> Tuple[List[float], Optional[str]]:
"""Validates coordinate dimensionality and executes single-point transformation.
Enforces:
-Finite numeric coordinate values (rejecting NaN / Infinity with empty coos).
-Strict 2D or 3D dimensionality (:math:`2 \\le \\text{dim} \\le 3`), echoing input
coordinates with ``"Wrong dimension"`` when :math:`\\text{dim} < 2` or :math:`\\text{dim} > 3`.
-Unit conversion between radians and decimal degrees.
-Domain exception mapping (``OutOfGridErr``, ``NoDataGridErr``, ``InvalidCoordinateErr``,
``WrongDimensionErr``) to standard warning strings.
:param trans: Pre-warmed TransRO instance
:type trans: TransRO
:param op: Operation name (e.g. Stereo70ToETRS89, ETRS89ToStereo70, Stereo30ToETRS89)
:type op: str
:param coords: Sequence of numeric coordinate values
:type coords: Sequence[float]
:param unit: Angular unit for ETRS89 coordinates (radians or degrees)
:type unit: AngleUnitEnum
:param source: Telemetry client source ID
:type source: int
:return: Tuple of ``(output_coords, warning_or_none)``
:rtype: Tuple[List[float], Optional[str]]
"""
coord_list, parse_warn = parse_numeric_coord_list(coords)
if parse_warn is not None:
return [], parse_warn
dim = len(coord_list)
if dim < 2 or dim > 3:
return coord_list, WARNING_WRONG_DIMENSION
if op in ("Stereo30ToETRS89", "ETRS89ToStereo30"):
return coord_list, STEREO30_WARNING
is_degrees = unit == AngleUnitEnum.DEGREES
try:
if op == OperationEnum.STEREO70_TO_ETRS89.value:
n, e = coord_list[0], coord_list[1]
z = coord_list[2] if dim == 3 else None
res = trans.st70_to_etrs89(n, e, z=z, source=source)
if is_degrees:
out_coords = [math.degrees(res[0]), math.degrees(res[1])]
if dim == 3:
out_coords.append(res[2]) # type: ignore[index]
return out_coords, None
return list(res), None
elif op == OperationEnum.ETRS89_TO_STEREO70.value:
lat, lon = coord_list[0], coord_list[1]
if is_degrees:
lat = math.radians(lat)
lon = math.radians(lon)
h = coord_list[2] if dim == 3 else None
res = trans.etrs89_to_st70(lat, lon, h=h, source=source)
return list(res), None
else:
raise ValueError(f"There is no defined transformation with name {op}")
except exc.OutOfGridErr:
return coord_list, WARNING_OUT_OF_GRID
except exc.NoDataGridErr:
return coord_list, WARNING_NO_DATA
except exc.WrongDimensionErr:
return coord_list, WARNING_WRONG_DIMENSION
except exc.InvalidCoordinateErr:
return [], WARNING_INVALID_COORDINATE
def _map_domain_exception_to_warning(domain_err: exc.PyTransDatError) -> str:
"""Maps a pytransdatro domain exception to its canonical API warning string."""
if isinstance(domain_err, exc.OutOfGridErr):
return WARNING_OUT_OF_GRID
if isinstance(domain_err, exc.NoDataGridErr):
return WARNING_NO_DATA
if isinstance(domain_err, exc.WrongDimensionErr):
return WARNING_WRONG_DIMENSION
return WARNING_INVALID_COORDINATE
def execute_batch_transformation(
trans: TransRO,
op: str,
points: Sequence[Any],
unit: AngleUnitEnum = AngleUnitEnum.RADIANS,
source: int = 1,
) -> List[Tuple[List[float], Optional[str]]]:
"""Executes fault-isolated batch coordinate transformation via TransRO.transform_batch.
:param trans: Pre-warmed TransRO instance
:type trans: TransRO
:param op: Operation name (e.g. Stereo70ToETRS89, ETRS89ToStereo70, Stereo30ToETRS89)
:type op: str
:param points: Sequence of coordinate rows
:type points: Sequence[Any]
:param unit: Angular unit for ETRS89 coordinates (radians or degrees)
:type unit: AngleUnitEnum
:param source: Telemetry client source ID
:type source: int
:return: Ordered list of ``(output_coords, warning_or_none)`` tuples
:rtype: List[Tuple[List[float], Optional[str]]]
"""
if op in ("Stereo30ToETRS89", "ETRS89ToStereo30"):
st30_results: List[Tuple[List[float], Optional[str]]] = []
for raw_pt in points:
coord_list, parse_warn = parse_numeric_coord_list(raw_pt)
if parse_warn is not None:
st30_results.append(([], parse_warn))
elif len(coord_list) < 2 or len(coord_list) > 3:
st30_results.append((coord_list, WARNING_WRONG_DIMENSION))
else:
st30_results.append((coord_list, STEREO30_WARNING))
return st30_results
if op not in (
OperationEnum.STEREO70_TO_ETRS89.value,
OperationEnum.ETRS89_TO_STEREO70.value,
):
raise ValueError(f"There is no defined transformation with name {op}")
from_st70 = op == OperationEnum.STEREO70_TO_ETRS89.value
is_degrees = unit == AngleUnitEnum.DEGREES
raw_results = trans.transform_batch(
points, from_st70=from_st70, is_degrees=is_degrees, source=source
)
out: List[Tuple[List[float], Optional[str]]] = []
for coords_out, domain_err in raw_results:
if domain_err is None:
out.append((coords_out, None))
else:
out.append((coords_out, _map_domain_exception_to_warning(domain_err)))
return out
def format_legacy_dict(coos: List[float], warning: Optional[str]) -> Dict[str, Any]:
"""Formats a transformation result into the legacy TransDatOnline dictionary contract.
Omits the ``warning`` key when ``warning`` is None.
"""
if warning is None:
return {"coos": coos}
return {"coos": coos, "warning": warning}
def format_modern_response(coos: List[float], warning: Optional[str]) -> PointResponse:
"""Formats a transformation result into a modern PointResponse schema instance."""
return PointResponse(coos=coos, warning=warning)