Source code for euring.coordinates
from euring.exceptions import EuringConstraintException
[docs]
def lat_lng_to_euring_coordinates(lat: float, lng: float) -> str:
"""Format latitude and longitude as EURING geographical coordinates."""
return f"{_lat_to_euring_coordinate(lat)}{_lng_to_euring_coordinate(lng)}"
[docs]
def euring_coordinates_to_lat_lng(value: str) -> dict[str, float]:
"""Parse EURING geographical coordinates into latitude/longitude decimals."""
lat_str = value[:7]
lng_str = value[7:]
return dict(lat=_euring_coordinate_to_decimal(lat_str), lng=_euring_coordinate_to_decimal(lng_str))
def _euring_coordinate_to_decimal(value: str) -> float:
"""Convert EURING geographical coordinate string to decimal coordinate."""
try:
seconds = value[-2:]
minutes = value[-4:-2]
degrees = value[:-4]
result = float(degrees)
negative = result < 0
result = abs(result) + (float(minutes) / 60) + (float(seconds) / 3600)
if negative:
result = -result
except (IndexError, ValueError):
raise EuringConstraintException('Could not parse coordinate "{value}" to decimal.')
return result
def _decimal_to_euring_coordinate(value: float, degrees_pos: int) -> str:
"""Format a decimal coordinate into EURING DMS text with fixed degree width."""
parts = _decimal_to_euring_coordinate_components(value)
return "{quadrant}{degrees}{minutes}{seconds}".format(
quadrant=parts["quadrant"],
degrees="{}".format(abs(parts["degrees"])).zfill(degrees_pos),
minutes="{}".format(parts["minutes"]).zfill(2),
seconds="{}".format(parts["seconds"]).zfill(2),
)
def _lat_to_euring_coordinate(value: float) -> str:
"""Convert a latitude in decimal degrees to a EURING coordinate string."""
return _decimal_to_euring_coordinate(value, degrees_pos=2)
def _lng_to_euring_coordinate(value: float) -> str:
"""Convert a longitude in decimal degrees to a EURING DMS coordinae string."""
return _decimal_to_euring_coordinate(value, degrees_pos=3)
def _decimal_to_euring_coordinate_components(value: float) -> dict[str, int | float | str]:
"""Convert a decimal coordinate into EURING geographical coordinate components."""
degrees = int(value)
submin = abs((value - int(value)) * 60)
minutes = int(submin)
seconds = abs((submin - int(submin)) * 60)
quadrant = "-" if degrees < 0 else "+"
seconds = int(round(seconds))
if seconds == 60:
seconds = 0
minutes += 1
if minutes == 60:
minutes = 0
degrees = degrees + 1 if degrees >= 0 else degrees - 1
return {"quadrant": quadrant, "degrees": degrees, "minutes": minutes, "seconds": seconds}
def _validate_euring_coordinates(value: str | None) -> None:
"""Validate a combined EURING latitude/longitude coordinate string."""
if value is None:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
if len(value) != 15:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
_validate_euring_coordinate_component(value[:7], degrees_digits=2, max_degrees=90)
_validate_euring_coordinate_component(value[7:], degrees_digits=3, max_degrees=180)
def _validate_euring_coordinate_component(value: str | None, *, degrees_digits: int, max_degrees: int) -> None:
"""Validate a single EURING coordinate component."""
if value is None:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
expected_length = 1 + degrees_digits + 2 + 2
if len(value) != expected_length:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
sign = value[0]
if sign not in {"+", "-"}:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
degrees = value[1 : 1 + degrees_digits]
minutes = value[1 + degrees_digits : 1 + degrees_digits + 2]
seconds = value[1 + degrees_digits + 2 :]
if not (degrees.isdigit() and minutes.isdigit() and seconds.isdigit()):
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')
if int(degrees) > max_degrees or int(minutes) > 59 or int(seconds) > 59:
raise EuringConstraintException(f'Value "{value}" is not a valid set of coordinates.')