|
86 | 86 | 'dms_to_decimal', |
87 | 87 | "to_utm", |
88 | 88 | "to_ll", |
| 89 | + "normalize_lat_lon", |
89 | 90 | 'GisError' |
90 | 91 | ] |
91 | 92 |
|
| 93 | +@overload |
| 94 | +def normalize_lat_lon( |
| 95 | + a: Union[float, str], |
| 96 | + b: Union[float, str], |
| 97 | + *, |
| 98 | + assume: Literal["lonlat", "latlon", "auto"] = ..., |
| 99 | + error: Literal["ignore", "raise"] = ..., |
| 100 | + clip: bool = ..., |
| 101 | +) -> Tuple[float, float]: |
| 102 | + ... |
| 103 | + |
| 104 | + |
| 105 | +@overload |
| 106 | +def normalize_lat_lon( |
| 107 | + a: Sequence[Union[float, str]], |
| 108 | + b: Sequence[Union[float, str]], |
| 109 | + *, |
| 110 | + assume: Literal["lonlat", "latlon", "auto"] = ..., |
| 111 | + error: Literal["ignore", "raise"] = ..., |
| 112 | + clip: bool = ..., |
| 113 | +) -> Tuple[np.ndarray, np.ndarray]: |
| 114 | + ... |
| 115 | + |
| 116 | +def normalize_lat_lon( |
| 117 | + a: Any, |
| 118 | + b: Any, |
| 119 | + *, |
| 120 | + assume: Literal["lonlat", "latlon", "auto"] = "lonlat", |
| 121 | + error: Literal["ignore", "raise"] = "ignore", |
| 122 | + clip: bool = False, |
| 123 | +): |
| 124 | + r""" |
| 125 | + Resolve input pair to ``(lat, lon)`` regardless of order. |
| 126 | +
|
| 127 | + Accepts values in either legacy order (lat, lon) or the |
| 128 | + new expected order (lon, lat). Returns a tuple in the |
| 129 | + canonical order ``(lat, lon)`` and avoids common |
| 130 | + ``|Latitude| > 90`` errors by swapping when clear. |
| 131 | +
|
| 132 | + Parameters |
| 133 | + ---------- |
| 134 | + a, b : float or str, or 1-D sequences |
| 135 | + Coordinate components. Strings may be decimal or DMS |
| 136 | + (``"DD:MM:SS"``). Sequences must be same length. |
| 137 | + assume : {"lonlat","latlon","auto"}, default "lonlat" |
| 138 | + Tie-break for ambiguous pairs (both ``|val| <= 90``). |
| 139 | + |
| 140 | + - ``"lonlat"``: treat input as (lon, lat). |
| 141 | + - ``"latlon"``: treat input as (lat, lon). |
| 142 | + - ``"auto"``: prefer (lon, lat) when ambiguous. |
| 143 | + |
| 144 | + error : {"ignore","raise"}, default "ignore" |
| 145 | + On impossible pairs (both ``|val| > 90`` or both |
| 146 | + ``|val| > 180``), either raise or return ``nan``s. |
| 147 | + clip : bool, default False |
| 148 | + If ``True``, clip lat to [-90, 90] and lon to |
| 149 | + [-180, 180] when slightly out-of-range. |
| 150 | +
|
| 151 | + Returns |
| 152 | + ------- |
| 153 | + lat, lon : float or ndarray |
| 154 | + Coordinates in canonical order. |
| 155 | +
|
| 156 | + Notes |
| 157 | + ----- |
| 158 | + Heuristics: |
| 159 | +
|
| 160 | + - If one value is in (90, 180] and the other in [-90, 90], |
| 161 | + the former is lon and the latter is lat. |
| 162 | + - If both are ``|val| <= 90``, use ``assume``. |
| 163 | + - Values with ``|val| > 180`` are invalid unless ``clip``. |
| 164 | + """ |
| 165 | + def _to_float(v): |
| 166 | + if v is None or v == "None": |
| 167 | + return np.nan |
| 168 | + if isinstance(v, str): |
| 169 | + try: |
| 170 | + return float(v) |
| 171 | + except Exception: |
| 172 | + try: |
| 173 | + return convert_position_str2float(v) # type: ignore # noqa: E501 |
| 174 | + except Exception: |
| 175 | + return np.nan |
| 176 | + try: |
| 177 | + return float(v) |
| 178 | + except Exception: |
| 179 | + return np.nan |
| 180 | + |
| 181 | + def _coerce_pair(x, y): |
| 182 | + xv = _to_float(x) |
| 183 | + yv = _to_float(y) |
| 184 | + |
| 185 | + ax = abs(xv) |
| 186 | + ay = abs(yv) |
| 187 | + |
| 188 | + # invalid domain checks |
| 189 | + if ax > 1e9 or ay > 1e9: |
| 190 | + xv = np.nan |
| 191 | + yv = np.nan |
| 192 | + |
| 193 | + # quick invalid > 180 |
| 194 | + if ax > 180 or ay > 180: |
| 195 | + if clip: |
| 196 | + xv = np.clip(xv, -180.0, 180.0) |
| 197 | + yv = np.clip(yv, -180.0, 180.0) |
| 198 | + ax = abs(xv) |
| 199 | + ay = abs(yv) |
| 200 | + else: |
| 201 | + if error == "raise": |
| 202 | + raise ValueError("Values exceed 180.") |
| 203 | + return (np.nan, np.nan) |
| 204 | + |
| 205 | + # decisive cases |
| 206 | + if (90 < ax <= 180) and (ay <= 90): |
| 207 | + lon = xv |
| 208 | + lat = yv |
| 209 | + return (lat, lon) |
| 210 | + if (90 < ay <= 180) and (ax <= 90): |
| 211 | + lon = yv |
| 212 | + lat = xv |
| 213 | + return (lat, lon) |
| 214 | + |
| 215 | + # both within 90 → ambiguous |
| 216 | + if ax <= 90 and ay <= 90: |
| 217 | + if assume == "latlon": |
| 218 | + lat, lon = xv, yv |
| 219 | + else: |
| 220 | + # "lonlat" and "auto" prefer lon,lat input |
| 221 | + lat, lon = yv, xv |
| 222 | + return (lat, lon) |
| 223 | + |
| 224 | + # one slightly out of 90 but not decisive |
| 225 | + if clip: |
| 226 | + xv = np.clip(xv, -180.0, 180.0) |
| 227 | + yv = np.clip(yv, -180.0, 180.0) |
| 228 | + ax = abs(xv) |
| 229 | + ay = abs(yv) |
| 230 | + |
| 231 | + # fallback: try assume |
| 232 | + if assume == "latlon": |
| 233 | + lat, lon = xv, yv |
| 234 | + else: |
| 235 | + lat, lon = yv, xv |
| 236 | + |
| 237 | + # final range checks |
| 238 | + if abs(lat) > 90 or abs(lon) > 180: |
| 239 | + if error == "raise": |
| 240 | + raise ValueError("Unresolvable pair.") |
| 241 | + return (np.nan, np.nan) |
| 242 | + |
| 243 | + return (lat, lon) |
| 244 | + |
| 245 | + # scalar vs vector handling |
| 246 | + if np.isscalar(a) and np.isscalar(b): |
| 247 | + return _coerce_pair(a, b) |
| 248 | + |
| 249 | + a_arr = np.asarray(a, dtype=object) |
| 250 | + b_arr = np.asarray(b, dtype=object) |
| 251 | + |
| 252 | + if a_arr.shape != b_arr.shape: |
| 253 | + raise ValueError("Shapes of a and b must match.") |
| 254 | + |
| 255 | + lat_out = np.empty_like(a_arr, dtype=float) |
| 256 | + lon_out = np.empty_like(b_arr, dtype=float) |
| 257 | + |
| 258 | + it = np.nditer( |
| 259 | + [a_arr, b_arr, lat_out, lon_out], |
| 260 | + flags=["multi_index", "refs_ok"], |
| 261 | + op_flags=[["readonly"], ["readonly"], |
| 262 | + ["writeonly"], ["writeonly"]], |
| 263 | + ) |
| 264 | + for xa, xb, yl, yo in it: |
| 265 | + lat, lon = _coerce_pair(xa.item(), xb.item()) |
| 266 | + yl[...] = lat |
| 267 | + yo[...] = lon |
| 268 | + |
| 269 | + return lat_out, lon_out |
| 270 | + |
92 | 271 | def assert_xy_coordinate_system(x, y) -> str: |
93 | 272 | r""" |
94 | 273 | Infer the coordinate system of paired ``x`` and ``y`` arrays. |
@@ -594,6 +773,7 @@ def convert_position_float2str(position: float) -> str: |
594 | 773 | ) |
595 | 774 | return position_str |
596 | 775 |
|
| 776 | + |
597 | 777 | @Deprecated( |
598 | 778 | "GDAL SpatialReference → UTM string is deprecated; " |
599 | 779 | "use 'get_utm_zone' for standard UTM formatting." |
|
0 commit comments