Module hxl.geo

Geodata operations

Functions for parsing latitudes and longitudes

May add more geo ops here later (e.g. filtering by boundaries).

Author

David Megginson

License

Public Domain

Expand source code
"""
Geodata operations

Functions for parsing latitudes and longitudes

May add more geo ops here later (e.g. filtering by boundaries).

Author:
    David Megginson

License:
    Public Domain

"""

import hxl, logging, re

__all__ = ["LAT_PATTERNS", "LON_PATTERNS", "parse_lat", "parse_lon", "parse_coord"]

logger = logging.getLogger(__name__)



########################################################################
# Constants
########################################################################

# regular expression fragments
_DEG_RE = r'(?P<deg>\d+(?:\.\d*)?)\s*\°?'
_MIN_RE = r'(?P<min>\d+(?:\.\d*)?)\s*[\'`′]?'
_SEC_RE = r'(?P<sec>\d+(?:\.\d*)?)\s*(?:["“”″]|[\'`′][\'`′])?'

LAT_PATTERNS = (
    re.compile(
        r'^(?P<sign>[+-])?\s*{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # -00 00 00
    re.compile(
        r'^(?P<hemi>[NS])\s*{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # S 00 00 00
    re.compile(
        r'^{}\s*(?P<hemi>[NS])\s*(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # 00 S 00 00
    re.compile(
        r'^{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)\s*(?P<hemi>[NS])?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # 00 00 00 S
)
"""List of regular expressions for parsing latitude strings"""


LON_PATTERNS = (
    re.compile(
        r'^(?P<sign>[+-])?\s*{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # -00 00 00
    re.compile(
        r'^(?P<hemi>[EW])\s*{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # S 00 00 00
    re.compile(
        r'^{}\s*(?P<hemi>[EW])\s*(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # 00 S 00 00
    re.compile(
        r'^{}(?:[\s:;,-]*{}(?:[\s:;,-]*{})?)\s*(?P<hemi>[EW])?$'.format(
            _DEG_RE, _MIN_RE, _SEC_RE
        ), flags=re.I
    ), # 00 00 00 S
)
"""List of regular expressions for parsing longitude strings"""



########################################################################
# Functions
########################################################################

def _make_degrees_digital(parts, max_deg):
    """Assemble the degrees, minutes, and seconds parts from a regular expression result into a decimal number.
    """
    num = float(parts['deg'])
    if num > max_deg or num < max_deg*-1:
        raise ValueError('degrees out of range {}/{}'.format(max_deg*-1, max_deg))
    if parts['min']:
        min = float(parts['min'])
        if min >= 60.0:
            raise ValueError('minutes must be less than 60')
        else:
            num += min/60.0
    if parts['sec']:
        sec = float(parts['sec'])
        if sec >= 60:
            raise ValueError('seconds must be less than 60')
        num += sec/3600.0
    if parts.get('sign') == '-' or (parts.get('hemi') and parts['hemi'].upper() in ('S', 'W')):
        num *= -1
    return num


def parse_lat(value):
    """Parse a latitude string

    Accepts a wide range of formats, as defined in LAT_PATTERNS

    Examples:
        ```
        lat = parse_lat("45.5000000") # => 45.5
        lat = parse_lat("45:30N") # => 45.5
        ```

    Args:
        value (str): the input string to parse

    Returns:
        float: decimal degrees latitude, or None if it can't be parsed.

    Raises:
        ValueError: if the input is out of allowed range
    
    """
    value = hxl.datatypes.normalise_space(value)
    for pattern in LAT_PATTERNS:
        result = re.match(pattern, value)
        if result:
            try:
                lat = _make_degrees_digital(result.groupdict(), max_deg=90)
            except ValueError as e:
                raise ValueError('failed to parse latitude {}: {}'.format(value, e.args[0]))
            return lat
    return None


def parse_lon(value):
    """Parse a longitude string

    Accepts a wide range of formats, as defined in LON_PATTERNS

    Examples:
        ```
        lon = parse_lon("-75.5000000") # => -75.5
        lon = parse_lon("75:30W") # => -75.5
        ```

    Args:
        value (str): the input string to parse

    Returns:
        float: decimal degrees longitude, or None if it can't be parsed.

    Raises:
        ValueError: if the input is out of allowed range
    
    """
    value = hxl.datatypes.normalise_space(value)
    for pattern in LON_PATTERNS:
        result = re.match(pattern, value)
        if result:
            try:
                lat = _make_degrees_digital(result.groupdict(), max_deg=180)
            except ValueError as e:
                raise ValueError('failed to parse latitude {}: {}'.format(value, e.args[0]))
            return lat
    return None


def parse_coord(value):
    """Parse lat/lon separated by a delimiter [/,:; ]

    Examples:
        ```
        coord = parse_coord("45.500000;-75.5000000") # => (45.5, -75.5,)
        coord = parse_coord("45:30N, 75:30W") # => (45.5, -75.5,)
        ```

    Args:
        value (str): the lat/lon coordinate string to parse

    Returns:
        tuple: the latitude and longitude as float values, or None if unparseable

    Raises:
        ValueError: if either of the coordinates is out of range
    """
    for delim in ('/', ',', ':', ';', ' ',):
        if value.find(delim) > 0:
            parts = value.split(delim)
            if len(parts) == 2:
                lat = parse_lat(parts[0])
                lon = parse_lon(parts[1])
                if lat and lon:
                    return (lat, lon,)
    return None

Global variables

var LAT_PATTERNS

List of regular expressions for parsing latitude strings

var LON_PATTERNS

List of regular expressions for parsing longitude strings

Functions

def parse_coord(value)

Parse lat/lon separated by a delimiter [/,:; ]

Examples

coord = parse_coord("45.500000;-75.5000000") # => (45.5, -75.5,)
coord = parse_coord("45:30N, 75:30W") # => (45.5, -75.5,)

Args

value : str
the lat/lon coordinate string to parse

Returns

tuple
the latitude and longitude as float values, or None if unparseable

Raises

ValueError
if either of the coordinates is out of range
Expand source code
def parse_coord(value):
    """Parse lat/lon separated by a delimiter [/,:; ]

    Examples:
        ```
        coord = parse_coord("45.500000;-75.5000000") # => (45.5, -75.5,)
        coord = parse_coord("45:30N, 75:30W") # => (45.5, -75.5,)
        ```

    Args:
        value (str): the lat/lon coordinate string to parse

    Returns:
        tuple: the latitude and longitude as float values, or None if unparseable

    Raises:
        ValueError: if either of the coordinates is out of range
    """
    for delim in ('/', ',', ':', ';', ' ',):
        if value.find(delim) > 0:
            parts = value.split(delim)
            if len(parts) == 2:
                lat = parse_lat(parts[0])
                lon = parse_lon(parts[1])
                if lat and lon:
                    return (lat, lon,)
    return None
def parse_lat(value)

Parse a latitude string

Accepts a wide range of formats, as defined in LAT_PATTERNS

Examples

lat = parse_lat("45.5000000") # => 45.5
lat = parse_lat("45:30N") # => 45.5

Args

value : str
the input string to parse

Returns

float
decimal degrees latitude, or None if it can't be parsed.

Raises

ValueError
if the input is out of allowed range
Expand source code
def parse_lat(value):
    """Parse a latitude string

    Accepts a wide range of formats, as defined in LAT_PATTERNS

    Examples:
        ```
        lat = parse_lat("45.5000000") # => 45.5
        lat = parse_lat("45:30N") # => 45.5
        ```

    Args:
        value (str): the input string to parse

    Returns:
        float: decimal degrees latitude, or None if it can't be parsed.

    Raises:
        ValueError: if the input is out of allowed range
    
    """
    value = hxl.datatypes.normalise_space(value)
    for pattern in LAT_PATTERNS:
        result = re.match(pattern, value)
        if result:
            try:
                lat = _make_degrees_digital(result.groupdict(), max_deg=90)
            except ValueError as e:
                raise ValueError('failed to parse latitude {}: {}'.format(value, e.args[0]))
            return lat
    return None
def parse_lon(value)

Parse a longitude string

Accepts a wide range of formats, as defined in LON_PATTERNS

Examples

lon = parse_lon("-75.5000000") # => -75.5
lon = parse_lon("75:30W") # => -75.5

Args

value : str
the input string to parse

Returns

float
decimal degrees longitude, or None if it can't be parsed.

Raises

ValueError
if the input is out of allowed range
Expand source code
def parse_lon(value):
    """Parse a longitude string

    Accepts a wide range of formats, as defined in LON_PATTERNS

    Examples:
        ```
        lon = parse_lon("-75.5000000") # => -75.5
        lon = parse_lon("75:30W") # => -75.5
        ```

    Args:
        value (str): the input string to parse

    Returns:
        float: decimal degrees longitude, or None if it can't be parsed.

    Raises:
        ValueError: if the input is out of allowed range
    
    """
    value = hxl.datatypes.normalise_space(value)
    for pattern in LON_PATTERNS:
        result = re.match(pattern, value)
        if result:
            try:
                lat = _make_degrees_digital(result.groupdict(), max_deg=180)
            except ValueError as e:
                raise ValueError('failed to parse latitude {}: {}'.format(value, e.args[0]))
            return lat
    return None