Skip to content
Snippets Groups Projects
geo.py 7.15 KiB
#!/usr/bin/env python
#
# geo.py is a python module with no dependencies on extra packages,
# providing some convenience functions for working with geographic
# coordinates
#
# Copyright (C) 2010  Maximilian Hoegner <hp.maxi@hoegners.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

### Part one - Functions for dealing with points on a sphere ###

### Part two - A tolerant parser for position strings ###
import re


class Parser:
    """ A parser class using regular expressions. """

    def __init__(self):
        self.patterns = {}
        self.raw_patterns = {}
        self.virtual = {}

    def add(self, name, pattern, virtual=False):
        """ Adds a new named pattern (regular expression) that can reference previously added patterns by %(pattern_name)s.
		Virtual patterns can be used to make expressions more compact but don't show up in the parse tree. """
        self.raw_patterns[name] = "(?:" + pattern + ")"
        self.virtual[name] = virtual

        try:
            self.patterns[name] = ("(?:" + pattern + ")") % self.patterns
        except KeyError as e:
            raise (Exception, "Unknown pattern name: %s" % str(e))

    def parse(self, pattern_name, text):
        """ Parses 'text' with pattern 'pattern_name' and returns parse tree """

        # build pattern with subgroups
        sub_dict = {}
        subpattern_names = []
        for s in re.finditer("%\(.*?\)s", self.raw_patterns[pattern_name]):
            subpattern_name = s.group()[2:-2]
            if not self.virtual[subpattern_name]:
                sub_dict[subpattern_name] = "(" + self.patterns[
                    subpattern_name] + ")"
                subpattern_names.append(subpattern_name)
            else:
                sub_dict[subpattern_name] = self.patterns[subpattern_name]

        pattern = "^" + (self.raw_patterns[pattern_name] % sub_dict) + "$"

        # do matching
        m = re.match(pattern, text)

        if m == None:
            return None

        # build tree recursively by parsing subgroups
        tree = {"TEXT": text}

        for i in range(len(subpattern_names)):
            text_part = m.group(i + 1)
            if not text_part == None:
                subpattern = subpattern_names[i]
                tree[subpattern] = self.parse(subpattern, text_part)

        return tree


position_parser = Parser()
position_parser.add("direction_ns", r"[NSns]")
position_parser.add("direction_ew", r"[EOWeow]")
position_parser.add("decimal_separator", r"[\.,]", True)
position_parser.add("sign", r"[+-]")

position_parser.add("nmea_style_degrees", r"[0-9]{2,}")
position_parser.add("nmea_style_minutes",
                    r"[0-9]{2}(?:%(decimal_separator)s[0-9]*)?")
position_parser.add(
    "nmea_style", r"%(sign)s?\s*%(nmea_style_degrees)s%(nmea_style_minutes)s")

position_parser.add(
    "number",
    r"[0-9]+(?:%(decimal_separator)s[0-9]*)?|%(decimal_separator)s[0-9]+")

position_parser.add("plain_degrees", r"(?:%(sign)s\s*)?%(number)s")

position_parser.add("degree_symbol", r"°", True)
position_parser.add("minutes_symbol", r"'|′|`|´", True)
position_parser.add("seconds_symbol",
                    r"%(minutes_symbol)s%(minutes_symbol)s|″|\"",
                    True)
position_parser.add("degrees", r"%(number)s\s*%(degree_symbol)s")
position_parser.add("minutes", r"%(number)s\s*%(minutes_symbol)s")
position_parser.add("seconds", r"%(number)s\s*%(seconds_symbol)s")
position_parser.add(
    "degree_coordinates",
    "(?:%(sign)s\s*)?%(degrees)s(?:[+\s]*%(minutes)s)?(?:[+\s]*%(seconds)s)?|(?:%(sign)s\s*)%(minutes)s(?:[+\s]*%(seconds)s)?|(?:%(sign)s\s*)%(seconds)s"
)

position_parser.add(
    "coordinates_ns",
    r"%(nmea_style)s|%(plain_degrees)s|%(degree_coordinates)s")
position_parser.add(
    "coordinates_ew",
    r"%(nmea_style)s|%(plain_degrees)s|%(degree_coordinates)s")

position_parser.add(
    "position", """\
\s*%(direction_ns)s\s*%(coordinates_ns)s[,;\s]*%(direction_ew)s\s*%(coordinates_ew)s\s*|\
\s*%(direction_ew)s\s*%(coordinates_ew)s[,;\s]*%(direction_ns)s\s*%(coordinates_ns)s\s*|\
\s*%(coordinates_ns)s\s*%(direction_ns)s[,;\s]*%(coordinates_ew)s\s*%(direction_ew)s\s*|\
\s*%(coordinates_ew)s\s*%(direction_ew)s[,;\s]*%(coordinates_ns)s\s*%(direction_ns)s\s*|\
\s*%(coordinates_ns)s[,;\s]+%(coordinates_ew)s\s*\
""")


def get_number(b):
    """ Takes appropriate branch of parse tree and returns float. """
    s = b["TEXT"].replace(",", ".")
    return float(s)


def get_coordinate(b):
    """ Takes appropriate branch of the parse tree and returns degrees as a float. """

    r = 0.

    if b.get("nmea_style"):
        if b["nmea_style"].get("nmea_style_degrees"):
            r += get_number(b["nmea_style"]["nmea_style_degrees"])
        if b["nmea_style"].get("nmea_style_minutes"):
            r += get_number(b["nmea_style"]["nmea_style_minutes"]) / 60.
        if b["nmea_style"].get(
                "sign") and b["nmea_style"]["sign"]["TEXT"] == "-":
            r *= -1.
    elif b.get("plain_degrees"):
        r += get_number(b["plain_degrees"]["number"])
        if b["plain_degrees"].get(
                "sign") and b["plain_degrees"]["sign"]["TEXT"] == "-":
            r *= -1.
    elif b.get("degree_coordinates"):
        if b["degree_coordinates"].get("degrees"):
            r += get_number(b["degree_coordinates"]["degrees"]["number"])
        if b["degree_coordinates"].get("minutes"):
            r += get_number(b["degree_coordinates"]["minutes"]["number"]) / 60.
        if b["degree_coordinates"].get("seconds"):
            r += get_number(
                b["degree_coordinates"]["seconds"]["number"]) / 3600.
        if b["degree_coordinates"].get(
                "sign") and b["degree_coordinates"]["sign"]["TEXT"] == "-":
            r *= -1.

    return r


def parse_position(s):
    """ Takes a (utf8-encoded) string describing a position and returns a tuple of floats for latitude and longitude in degrees.
	Tries to be as tolerant as possible with input. Returns None if parsing doesn't succeed. """

    parse_tree = position_parser.parse("position", s)
    if parse_tree == None: return None

    lat_sign = +1.
    if parse_tree.get(
            "direction_ns") and parse_tree["direction_ns"]["TEXT"] in ("S",
                                                                       "s"):
        lat_sign = -1.

    lon_sign = +1.
    if parse_tree.get(
            "direction_ew") and parse_tree["direction_ew"]["TEXT"] in ("W",
                                                                       "w"):
        lon_sign = -1.

    lat = lat_sign * get_coordinate(parse_tree["coordinates_ns"])
    lon = lon_sign * get_coordinate(parse_tree["coordinates_ew"])

    return lat, lon