|
| 1 | +from euring.exceptions import EuringConstraintException |
| 2 | + |
| 3 | + |
| 4 | +def lat_lng_to_euring_coordinates(lat: float, lng: float) -> str: |
| 5 | + """Format latitude and longitude as EURING geographical coordinates.""" |
| 6 | + return f"{_lat_to_euring_coordinate(lat)}{_lng_to_euring_coordinate(lng)}" |
| 7 | + |
| 8 | + |
| 9 | +def euring_coordinates_to_lat_lng(value: str) -> dict[str, float]: |
| 10 | + """Parse EURING geographical coordinates into latitude/longitude decimals.""" |
| 11 | + lat_str = value[:7] |
| 12 | + lng_str = value[7:] |
| 13 | + return dict(lat=_euring_coordinate_to_decimal(lat_str), lng=_euring_coordinate_to_decimal(lng_str)) |
| 14 | + |
| 15 | + |
| 16 | +def _euring_coordinate_to_decimal(value: str) -> float: |
| 17 | + """Convert EURING geographical coordinate string to decimal coordinate.""" |
| 18 | + try: |
| 19 | + seconds = value[-2:] |
| 20 | + minutes = value[-4:-2] |
| 21 | + degrees = value[:-4] |
| 22 | + result = float(degrees) |
| 23 | + negative = result < 0 |
| 24 | + result = abs(result) + (float(minutes) / 60) + (float(seconds) / 3600) |
| 25 | + if negative: |
| 26 | + result = -result |
| 27 | + except (IndexError, ValueError): |
| 28 | + raise EuringConstraintException('Could not parse coordinate "{value}" to decimal.') |
| 29 | + return result |
| 30 | + |
| 31 | + |
| 32 | +def _decimal_to_euring_coordinate(value: float, degrees_pos: int) -> str: |
| 33 | + """Format a decimal coordinate into EURING DMS text with fixed degree width.""" |
| 34 | + parts = _decimal_to_euring_coordinate_components(value) |
| 35 | + return "{quadrant}{degrees}{minutes}{seconds}".format( |
| 36 | + quadrant=parts["quadrant"], |
| 37 | + degrees="{}".format(abs(parts["degrees"])).zfill(degrees_pos), |
| 38 | + minutes="{}".format(parts["minutes"]).zfill(2), |
| 39 | + seconds="{}".format(parts["seconds"]).zfill(2), |
| 40 | + ) |
| 41 | + |
| 42 | + |
| 43 | +def _lat_to_euring_coordinate(value: float) -> str: |
| 44 | + """Convert a latitude in decimal degrees to a EURING coordinate string.""" |
| 45 | + return _decimal_to_euring_coordinate(value, degrees_pos=2) |
| 46 | + |
| 47 | + |
| 48 | +def _lng_to_euring_coordinate(value: float) -> str: |
| 49 | + """Convert a longitude in decimal degrees to a EURING DMS coordinae string.""" |
| 50 | + return _decimal_to_euring_coordinate(value, degrees_pos=3) |
| 51 | + |
| 52 | + |
| 53 | +def _decimal_to_euring_coordinate_components(value: float) -> dict[str, int | float | str]: |
| 54 | + """Convert a decimal coordinate into EURING geographical coordinate components.""" |
| 55 | + degrees = int(value) |
| 56 | + submin = abs((value - int(value)) * 60) |
| 57 | + minutes = int(submin) |
| 58 | + seconds = abs((submin - int(submin)) * 60) |
| 59 | + quadrant = "-" if degrees < 0 else "+" |
| 60 | + seconds = int(round(seconds)) |
| 61 | + if seconds == 60: |
| 62 | + seconds = 0 |
| 63 | + minutes += 1 |
| 64 | + if minutes == 60: |
| 65 | + minutes = 0 |
| 66 | + degrees = degrees + 1 if degrees >= 0 else degrees - 1 |
| 67 | + return {"quadrant": quadrant, "degrees": degrees, "minutes": minutes, "seconds": seconds} |
| 68 | + |
| 69 | + |
| 70 | +def _validate_euring_coordinates(value: str | None) -> None: |
| 71 | + """Validate a combined EURING latitude/longitude coordinate string.""" |
| 72 | + if value is None: |
| 73 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 74 | + if len(value) != 15: |
| 75 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 76 | + _validate_euring_coordinate_component(value[:7], degrees_digits=2, max_degrees=90) |
| 77 | + _validate_euring_coordinate_component(value[7:], degrees_digits=3, max_degrees=180) |
| 78 | + |
| 79 | + |
| 80 | +def _validate_euring_coordinate_component(value: str | None, *, degrees_digits: int, max_degrees: int) -> None: |
| 81 | + """Validate a single EURING coordinate component.""" |
| 82 | + if value is None: |
| 83 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 84 | + expected_length = 1 + degrees_digits + 2 + 2 |
| 85 | + if len(value) != expected_length: |
| 86 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 87 | + sign = value[0] |
| 88 | + if sign not in {"+", "-"}: |
| 89 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 90 | + degrees = value[1 : 1 + degrees_digits] |
| 91 | + minutes = value[1 + degrees_digits : 1 + degrees_digits + 2] |
| 92 | + seconds = value[1 + degrees_digits + 2 :] |
| 93 | + if not (degrees.isdigit() and minutes.isdigit() and seconds.isdigit()): |
| 94 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
| 95 | + if int(degrees) > max_degrees or int(minutes) > 59 or int(seconds) > 59: |
| 96 | + raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.') |
0 commit comments