Source code for upoints.osm

#
# coding=utf-8
"""osm - Imports OpenStreetMap data files.."""
# Copyright © 2008-2017  James Rowe <jnrowe@gmail.com>
#
# This file is part of upoints.
#
# upoints 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.
#
# upoints 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
# upoints.  If not, see <http://www.gnu.org/licenses/>.

from operator import attrgetter

try:
    from urllib.request import urlopen
except ImportError:  # Python 2
    from urllib import urlopen

from lxml import etree

from . import (point, utils)
from ._version import web as ua_string
from .compat import mangle_repr_type

create_elem = utils.element_creator()


def _parse_flags(element):
    """Parse OSM XML element for generic data.

    Args:
        element (etree.Element): Element to parse

    Returns:
        tuple: Generic OSM data for object instantiation
    """
    visible = True if element.get('visible') else False
    user = element.get('user')
    timestamp = element.get('timestamp')
    if timestamp:
        timestamp = utils.Timestamp.parse_isoformat(timestamp)
    tags = {}
    try:
        for tag in element['tag']:
            key = tag.get('k')
            value = tag.get('v')
            tags[key] = value
    except AttributeError:
        pass

    return visible, user, timestamp, tags


def _get_flags(osm_obj):
    """Create element independent flags output.

    Args:
        osm_obj (Node): Object with OSM-style metadata

    Returns:
        list: Human readable flags output
    """
    flags = []
    if osm_obj.visible:
        flags.append('visible')
    if osm_obj.user:
        flags.append('user: %s' % osm_obj.user)
    if osm_obj.timestamp:
        flags.append('timestamp: %s' % osm_obj.timestamp.isoformat())
    if osm_obj.tags:
        flags.append(', '.join('%s: %s' % (k, v)
                               for k, v in sorted(osm_obj.tags.items())))
    return flags


[docs]def get_area_url(location, distance): """Generate URL for downloading OSM data within a region. This function defines a boundary box where the edges touch a circle of ``distance`` kilometres in radius. It is important to note that the box is neither a square, nor bounded within the circle. The bounding box is strictly a trapezoid whose north and south edges are different lengths, which is longer is dependant on whether the box is calculated for a location in the Northern or Southern hemisphere. You will get a shorter north edge in the Northern hemisphere, and vice versa. This is simply because we are applying a flat transformation to a spherical object, however for all general cases the difference will be negligible. Args: location (Point): Centre of the region distance (int): Boundary distance in kilometres Returns: str: URL that can be used to fetch the OSM data within ``distance`` of ``location`` """ locations = [location.destination(i, distance) for i in range(0, 360, 90)] latitudes = list(map(attrgetter('latitude'), locations)) longitudes = list(map(attrgetter('longitude'), locations)) bounds = (min(longitudes), min(latitudes), max(longitudes), max(latitudes)) return ('http://api.openstreetmap.org/api/0.5/map?bbox=' + ','.join(map(str, bounds)))
[docs]class Node(point.Point): """Class for representing a node element from OSM data files. .. versionadded:: 0.9.0 """ __slots__ = ('ident', 'visible', 'user', 'timestamp', 'tags') def __init__(self, ident, latitude, longitude, visible=False, user=None, timestamp=None, tags=None): """Initialise a new ``Node`` object. Args: ident (int): Unique identifier for the node latitude (float): Nodes's latitude longitude (float): Node's longitude visible (bool): Whether the node is visible user (str): User who logged the node timestamp (str): The date and time a node was logged tags (dict): Tags associated with the node """ super(Node, self).__init__(latitude, longitude) self.ident = ident self.visible = visible self.user = user self.timestamp = timestamp self.tags = tags def __str__(self): """Pretty printed location string. Returns: str: Human readable string representation of ``Node`` object """ text = ['Node %i (%s)' % (self.ident, super(Node, self).__format__('dms')), ] flags = _get_flags(self) if flags: text.append('[%s]' % ', '.join(flags)) return ' '.join(text)
[docs] def toosm(self): """Generate a OSM node element subtree. Returns: etree.Element: OSM node element """ node = create_elem('node', {'id': str(self.ident), 'lat': str(self.latitude), 'lon': str(self.longitude)}) node.set('visible', 'true' if self.visible else 'false') if self.user: node.set('user', self.user) if self.timestamp: node.set('timestamp', self.timestamp.isoformat()) if self.tags: for key, value in sorted(self.tags.items()): node.append(create_elem('tag', {'k': key, 'v': value})) return node
[docs] def get_area_url(self, distance): """Generate URL for downloading OSM data within a region. Args: distance (int): Boundary distance in kilometres Returns: str: URL that can be used to fetch the OSM data within ``distance`` of ``location`` """ return get_area_url(self, distance)
[docs] def fetch_area_osm(self, distance): """Fetch, and import, an OSM region. Args: distance (int): Boundary distance in kilometres Returns: Osm: All the data OSM has on a region imported for use """ return Osm(urlopen(get_area_url(self, distance)))
[docs] @staticmethod def parse_elem(element): """Parse a OSM node XML element. Args: element (etree.Element): XML Element to parse Returns: Node: Object representing parsed element """ ident = int(element.get('id')) latitude = element.get('lat') longitude = element.get('lon') flags = _parse_flags(element) return Node(ident, latitude, longitude, *flags)
[docs]@mangle_repr_type class Way(point.Points): """Class for representing a way element from OSM data files. .. versionadded:: 0.9.0 """ __slots__ = ('ident', 'visible', 'user', 'timestamp', 'tags') def __init__(self, ident, nodes, visible=False, user=None, timestamp=None, tags=None): """Initialise a new ``Way`` object. Args: ident (int): Unique identifier for the way nodes (list of str): Identifiers of the nodes that form this way visible (bool): Whether the way is visible user (str): User who logged the way timestamp (str): The date and time a way was logged tags (dict): Tags associated with the way """ super(Way, self).__init__() self.extend(nodes) self.ident = ident self.visible = visible self.user = user self.timestamp = timestamp self.tags = tags def __repr__(self): """Self-documenting string representation. Returns:: str: String to recreate ``Way`` object """ return utils.repr_assist(self, {'nodes': self[:]}) def __str__(self, nodes=False): """Pretty printed location string. Args: nodes (list): Nodes to be used in expanding references Returns: str: Human readable string representation of ``Way`` object """ text = ['Way %i' % (self.ident), ] if not nodes: text.append(' (nodes: %s)' % str(self[:])[1:-1]) flags = _get_flags(self) if flags: text.append(' [%s]' % ', '.join(flags)) if nodes: text.append('\n') text.append('\n'.join(' %s' % nodes[node] for node in self[:])) return ''.join(text)
[docs] def toosm(self): """Generate a OSM way element subtree. Returns: etree.Element: OSM way element """ way = create_elem('way', {'id': str(self.ident)}) way.set('visible', 'true' if self.visible else 'false') if self.user: way.set('user', self.user) if self.timestamp: way.set('timestamp', self.timestamp.isoformat()) if self.tags: for key, value in sorted(self.tags.items()): way.append(create_elem('tag', {'k': key, 'v': value})) for node in self: way.append(create_elem('nd', {'ref': str(node)})) return way
[docs] @staticmethod def parse_elem(element): """Parse a OSM way XML element. Args: element (etree.Element): XML Element to parse Returns: Way: `Way` object representing parsed element """ ident = int(element.get('id')) flags = _parse_flags(element) nodes = [node.get('ref') for node in element.findall('nd')] return Way(ident, nodes, *flags)
[docs]class Osm(point.Points): """Class for representing an OSM region. .. versionadded:: 0.9.0 """ def __init__(self, osm_file=None): """Initialise a new ``Osm`` object.""" super(Osm, self).__init__() self._osm_file = osm_file if osm_file: self.import_locations(osm_file) self.generator = ua_string self.version = '0.5'
[docs] def import_locations(self, osm_file): """Import OSM data files. ``import_locations()`` returns a list of ``Node`` and ``Way`` objects. It expects data files conforming to the `OpenStreetMap 0.5 DTD`_, which is XML such as:: <?xml version="1.0" encoding="UTF-8"?> <osm version="0.5" generator="upoints/0.9.0"> <node id="0" lat="52.015749" lon="-0.221765" user="jnrowe" visible="true" timestamp="2008-01-25T12:52:11+00:00" /> <node id="1" lat="52.015761" lon="-0.221767" visible="true" timestamp="2008-01-25T12:53:00+00:00"> <tag k="created_by" v="hand" /> <tag k="highway" v="crossing" /> </node> <node id="2" lat="52.015754" lon="-0.221766" user="jnrowe" visible="true" timestamp="2008-01-25T12:52:30+00:00"> <tag k="amenity" v="pub" /> </node> <way id="0" visible="true" timestamp="2008-01-25T13:00:00+0000"> <nd ref="0" /> <nd ref="1" /> <nd ref="2" /> <tag k="ref" v="My Way" /> <tag k="highway" v="primary" /> </way> </osm> The reader uses the :mod:`ElementTree` module, so should be very fast when importing data. The above file processed by ``import_locations()`` will return the following `Osm` object:: Osm([ Node(0, 52.015749, -0.221765, True, "jnrowe", utils.Timestamp(2008, 1, 25, 12, 52, 11), None), Node(1, 52.015761, -0.221767, True, utils.Timestamp(2008, 1, 25, 12, 53), None, {"created_by": "hand", "highway": "crossing"}), Node(2, 52.015754, -0.221766, True, "jnrowe", utils.Timestamp(2008, 1, 25, 12, 52, 30), {"amenity": "pub"}), Way(0, [0, 1, 2], True, None, utils.Timestamp(2008, 1, 25, 13, 00), {"ref": "My Way", "highway": "primary"})], generator="upoints/0.9.0") Args: osm_file (iter): OpenStreetMap data to read Returns: Osm: Nodes and ways from the data .. _OpenStreetMap 0.5 DTD: http://wiki.openstreetmap.org/wiki/OSM_Protocol_Version_0.5/DTD """ self._osm_file = osm_file data = utils.prepare_xml_read(osm_file, objectify=True) # This would be a lot simpler if OSM exports defined a namespace if not data.tag == 'osm': raise ValueError("Root element %r is not `osm'" % data.tag) self.version = data.get('version') if not self.version: raise ValueError('No specified OSM version') elif not self.version == '0.5': raise ValueError('Unsupported OSM version %r' % data) self.generator = data.get('generator') for elem in data.getchildren(): if elem.tag == 'node': self.append(Node.parse_elem(elem)) elif elem.tag == 'way': self.append(Way.parse_elem(elem))
[docs] def export_osm_file(self): """Generate OpenStreetMap element tree from ``Osm``.""" osm = create_elem('osm', {'generator': self.generator, 'version': self.version}) osm.extend(obj.toosm() for obj in self) return etree.ElementTree(osm)