libpostal-sys 0.1.1

Low-level wrappers for libpostal address normalization (with locks to support thread-safe initialization)
Documentation
# -*- 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°42′46″N', '74°00′21″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