Files
2025-09-06 22:03:29 -04:00

166 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
'''
geodata.coordinates.conversion
------------------------------
Geographic coordinates typically come in two flavors: decimal and
DMS (degree-minute-second). This module parses a coordinate string
in just about any format. This was originally created for parsing
lat/lons found on the web.
Usage:
>>> latlon_to_decimal('40°4246″N', '74°0021″W') # returns (40.71277777777778, 74.00583333333333)
>>> latlon_to_decimal('40,74 N', '74,001 W') # returns (40.74, -74.001)
>>> to_valid_longitude(360.0)
>>> latitude_is_valid(90.0)
'''
import math
import re
from geodata.encoding import safe_decode
from geodata.math.floats import isclose
beginning_re = re.compile('^[^0-9\-]+', re.UNICODE)
end_re = re.compile('[^0-9]+$', re.UNICODE)
latitude_dms_regex = re.compile(ur'^(-?[0-9]{1,2})[ ]*[ :°ºd][ ]*([0-5]?[0-9])?[ ]*[:\'\u2032m]?[ ]*([0-5]?[0-9](?:\.\d+)?)?[ ]*[:\?\"\u2033s]?[ ]*(N|n|S|s)?$', re.I | re.UNICODE)
longitude_dms_regex = re.compile(ur'^(-?1[0-8][0-9]|0?[0-9]{1,2})[ ]*[ :°ºd][ ]*([0-5]?[0-9])?[ ]*[:\'\u2032m]?[ ]*([0-5]?[0-9](?:\.\d+)?)?[ ]*[:\?\"\u2033s]?[ ]*(E|e|W|w)?$', re.I | re.UNICODE)
latitude_decimal_with_direction_regex = re.compile('^(-?[0-9][0-9](?:\.[0-9]+))[ ]*[ :°ºd]?[ ]*(N|n|S|s)$', re.I)
longitude_decimal_with_direction_regex = re.compile('^(-?1[0-8][0-9]|0?[0-9][0-9](?:\.[0-9]+))[ ]*[ :°ºd]?[ ]*(E|e|W|w)$', re.I)
direction_sign_map = {'n': 1, 's': -1, 'e': 1, 'w': -1}
def direction_sign(d):
if d is None:
return 1
d = d.lower().strip()
if d in direction_sign_map:
return direction_sign_map[d]
else:
raise ValueError('Invalid direction: {}'.format(d))
def int_or_float(d):
try:
return int(d)
except ValueError:
return float(d)
def degrees_to_decimal(degrees, minutes, seconds):
degrees = int_or_float(degrees)
minutes = int_or_float(minutes)
seconds = int_or_float(seconds)
return degrees + (minutes / 60.0) + (seconds / 3600.0)
def is_valid_latitude(latitude):
'''Latitude must be real number between -90.0 and 90.0'''
try:
latitude = float(latitude)
except (ValueError, TypeError):
return False
if latitude > 90.0 or latitude < -90.0 or math.isinf(latitude) or math.isnan(latitude):
return False
return True
def is_valid_longitude(longitude):
'''Allow any valid real number to be a longitude'''
try:
longitude = float(longitude)
except (ValueError, TypeError):
return False
return not math.isinf(longitude) and not math.isnan(longitude)
def to_valid_latitude(latitude):
'''Convert longitude into the -180 to 180 scale'''
if not is_valid_latitude(latitude):
raise ValueError('Invalid latitude {}'.format(latitude))
if isclose(latitude, 90.0):
latitude = 89.9999
elif isclose(latitude, -90.0):
latitude = -89.9999
return latitude
def to_valid_longitude(longitude):
'''Convert longitude into the -180 to 180 scale'''
if not is_valid_longitude(longitude):
raise ValueError('Invalid longitude {}'.format(longitude))
while longitude <= -180.0:
longitude += 360.0
while longitude > 180.0:
longitude -= 360.0
return longitude
def latlon_to_decimal(latitude, longitude):
have_lat = False
have_lon = False
latitude = safe_decode(latitude).strip(u' ,;|')
longitude = safe_decode(longitude).strip(u' ,;|')
latitude = latitude.replace(u',', u'.')
longitude = longitude.replace(u',', u'.')
lat_dms = latitude_dms_regex.match(latitude)
lat_dir = latitude_decimal_with_direction_regex.match(latitude)
if lat_dms:
d, m, s, c = lat_dms.groups()
sign = direction_sign(c)
latitude = degrees_to_decimal(d or 0, m or 0, s or 0)
have_lat = True
elif lat_dir:
d, c = lat_dir.groups()
sign = direction_sign(c)
latitude = return_type(d) * sign
have_lat = True
else:
latitude = re.sub(beginning_re, u'', latitude)
latitude = re.sub(end_re, u'', latitude)
lon_dms = longitude_dms_regex.match(longitude)
lon_dir = longitude_decimal_with_direction_regex.match(longitude)
if lon_dms:
d, m, s, c = lon_dms.groups()
sign = direction_sign(c)
longitude = degrees_to_decimal(d or 0, m or 0, s or 0)
have_lon = True
elif lon_dir:
d, c = lon_dir.groups()
sign = direction_sign(c)
longitude = return_type(d) * sign
have_lon = True
else:
longitude = re.sub(beginning_re, u'', longitude)
longitude = re.sub(end_re, u'', longitude)
latitude = float(latitude)
longitude = float(longitude)
if not is_valid_latitude(latitude):
raise ValueError('Invalid latitude: {}'.format(latitude))
if not is_valid_longitude(longitude):
raise ValueError('Invalid longitude: {}'.format(longitude))
latitude = to_valid_latitude(latitude)
longitude = to_valid_longitude(longitude)
return latitude, longitude