Skip to content
Snippets Groups Projects
imports.py 8.04 KiB
Newer Older
markn92's avatar
markn92 committed
"""
Convert a Open Street Maps `.map` format file into a networkx directional graph.

markn92's avatar
markn92 committed
This parser is based on the osm to networkx tool
from Loïc Messal (github : Tofull)
markn92's avatar
markn92 committed

Added :
- python3.6 compatibility
- networkx v2 compatibility
- cache to avoid downloading the same osm tiles again and again
- distance computation to estimate length of each ways (useful to compute the shortest path)

"""
import copy
import xml.sax
markn92's avatar
markn92 committed
import logging
markn92's avatar
markn92 committed
import itertools
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
import networkx as nx
markn92's avatar
markn92 committed
import rtree
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
from evrouting.graph_tools import CHARGING_COEFFICIENT_KEY, DISTANCE_KEY
markn92's avatar
markn92 committed
from evrouting.osm.const import ms_to_kmh
from evrouting.osm.profiles import speed
markn92's avatar
markn92 committed
from evrouting.osm.routing import point, haversine_distance
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
logger = logging.getLogger(__name__)
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
HAVERSINE_KEY = 'haversine'

markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
class OSMGraph(nx.DiGraph):
    """
    Adding some functionality to the graph for convenience when
    working with actual geo data from osm, such as a spatial index and
    a method to get an entry node by the spacial index for some coordinates.
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
    """
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.charging_stations = set()
markn92's avatar
markn92 committed

        # Data structures for spatial index.
        # Rtree only supports indexing by integers, but the
        # node ids are strings. Therefore a mapping is introduced.
markn92's avatar
markn92 committed
        self._rtree = rtree.index.Index()
        self._int_index = itertools.count()
markn92's avatar
markn92 committed
        self._int_index_map = {}
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
    def insert_into_rtree(self, node):
        """Insert node into rtree."""
markn92's avatar
markn92 committed
        info = self.nodes[node]
        lat, lon = info['lat'], info['lon']
markn92's avatar
markn92 committed
        index = next(self._int_index)
        self._rtree.insert(index, (lon, lat, lon, lat))
        self._int_index_map[index] = node

    def rebuild_rtree(self):
        """Rebuild tree. Necessary because it cannot be pickled."""
        self._rtree = rtree.index.Index()
        self._int_index = itertools.count()
        self._int_index_map = {}
        for n in self.nodes:
            self.insert_into_rtree(n)
markn92's avatar
markn92 committed

    def insert_charging_stations(self, charging_stations):
        """Insert Charging Stations"""
        S = set()
        for s in charging_stations:
            lon = s['lon']
            lat = s['lat']
            n = self.find_nearest((lat, lon), distance_limit=500)
            if n is not None:
                self.nodes[n][CHARGING_COEFFICIENT_KEY] = s['power']
                S.add(n)

        self.charging_stations = S

    def find_nearest(self, v: point, distance_limit=None):
        """
        Find nearest point to location v within radius
        of distance_limit.
        """
        lat_v, lon_v = v
markn92's avatar
markn92 committed
        index_n = next(self._rtree.nearest(
            (lon_v, lat_v, lon_v, lat_v), 1
        ))
        n = self._int_index_map[index_n]
markn92's avatar
markn92 committed
        if distance_limit is not None:
            d = haversine_distance(
                self.nodes[n]['lat'],
                self.nodes[n]['lon'],
                lat_v,
                lon_v,
                unit_m=True
            )
            if d > distance_limit:
                n = None
markn92's avatar
markn92 committed
        return n
markn92's avatar
markn92 committed
def read_osm(osm_xml_data, profile) -> OSMGraph:
markn92's avatar
markn92 committed
    """
    Read graph in OSM format from file specified by name or by stream object.
    Create Graph containing all highways as edges.
markn92's avatar
markn92 committed

    """
markn92's avatar
markn92 committed

    osm = OSM(osm_xml_data)
markn92's avatar
markn92 committed
    G = OSMGraph()
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
    # Add ways
markn92's avatar
markn92 committed
    for w in osm.ways.values():
markn92's avatar
markn92 committed
        if 'highway' not in w.tags:
markn92's avatar
markn92 committed
            continue
markn92's avatar
markn92 committed
        if w.tags['highway'] not in profile['highway_whitelist']:
            continue

        for u_id, v_id in zip(w.nds[:-1], w.nds[1:]):
            u, v = osm.nodes[u_id], osm.nodes[v_id]

            # Travel-time from u to v
markn92's avatar
markn92 committed
            d = haversine_distance(u.lon, u.lat, v.lon, v.lat, unit_m=True)  # in m
            t = d / (speed(w, profile) / ms_to_kmh)  # in s
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
            if w.tags.get('oneway', 'no') == 'yes':
markn92's avatar
markn92 committed
                # ONLY ONE DIRECTION
markn92's avatar
markn92 committed
                G.add_edge(u_id, v_id, **{
markn92's avatar
markn92 committed
                    DISTANCE_KEY: t,
                    HAVERSINE_KEY: d
markn92's avatar
markn92 committed
                })
markn92's avatar
markn92 committed
            else:
                # BOTH DIRECTION
markn92's avatar
markn92 committed
                G.add_edge(u_id, v_id, **{
markn92's avatar
markn92 committed
                    DISTANCE_KEY: t,
                    HAVERSINE_KEY: d
markn92's avatar
markn92 committed
                })
                G.add_edge(v_id, u_id, **{
markn92's avatar
markn92 committed
                    DISTANCE_KEY: t,
                    HAVERSINE_KEY: d
markn92's avatar
markn92 committed
                })
markn92's avatar
markn92 committed
    # Complete the used nodes' information
markn92's avatar
markn92 committed
    for n_id, data in G.nodes(data=True):
markn92's avatar
markn92 committed
        n = osm.nodes[n_id]
markn92's avatar
markn92 committed
        data['lat'] = n.lat
        data['lon'] = n.lon
        data['id'] = n.id
markn92's avatar
markn92 committed
        G.insert_into_rtree(n_id)
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
    return G
markn92's avatar
markn92 committed


class Node(object):
    def __init__(self, id, lon, lat):
        self.id = id
        self.lon = lon
        self.lat = lat
        self.tags = {}

    def __str__(self):
markn92's avatar
markn92 committed
        return "Node (id : %s) lon : %s, lat : %s " % (self.id, self.lon, self.lat)
markn92's avatar
markn92 committed


class Way(object):
markn92's avatar
markn92 committed
    def __init__(self, id):
markn92's avatar
markn92 committed
        self.id = id
        self.nds = []
        self.tags = {}

markn92's avatar
markn92 committed
    def split(self, node_pass_count):
        """
        Slice way at every crossing i.e. when a waypoint is passend by
        multiple ways.
        """

markn92's avatar
markn92 committed
        # slice the node-array using this nifty recursive function
markn92's avatar
markn92 committed
        def slice_array(waypoints):
            for i in range(1, len(waypoints) - 1):
                if node_pass_count[waypoints[i]] > 1:
                    left = waypoints[:i + 1]
                    right = waypoints[i:]
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
                    rightsliced = slice_array(right)
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
                    return [left] + rightsliced
markn92's avatar
markn92 committed
            return [waypoints]
markn92's avatar
markn92 committed

markn92's avatar
markn92 committed
        slices = slice_array(self.nds)
markn92's avatar
markn92 committed

        # create a way object for each node-array slice
        ret = []
        i = 0
        for slice in slices:
            littleway = copy.copy(self)
            littleway.id += "-%d" % i
            littleway.nds = slice
            ret.append(littleway)
            i += 1

        return ret


class OSM(object):
markn92's avatar
markn92 committed
    def __init__(self, osm_xml_data):
markn92's avatar
markn92 committed
        """ File can be either a filename or stream/file object.

        set `is_xml_string=False` if osm_xml_data is a filename or a file stream.
        """
        nodes = {}
        ways = {}

        class OSMHandler(xml.sax.ContentHandler):
markn92's avatar
markn92 committed
            def __init__(self):
                super().__init__()
                self.currElem = None

markn92's avatar
markn92 committed
            @classmethod
            def setDocumentLocator(self, loc):
                pass

            @classmethod
            def startDocument(self):
                pass

            @classmethod
            def endDocument(self):
                pass

            @classmethod
            def startElement(self, name, attrs):
                if name == 'node':
                    self.currElem = Node(attrs['id'], float(attrs['lon']), float(attrs['lat']))
                elif name == 'way':
markn92's avatar
markn92 committed
                    self.currElem = Way(attrs['id'])
markn92's avatar
markn92 committed
                elif name == 'tag':
                    self.currElem.tags[attrs['k']] = attrs['v']
                elif name == 'nd':
                    self.currElem.nds.append(attrs['ref'])

            @classmethod
            def endElement(self, name):
                if name == 'node':
                    nodes[self.currElem.id] = self.currElem
                elif name == 'way':
                    ways[self.currElem.id] = self.currElem

            @classmethod
            def characters(self, chars):
                pass

markn92's avatar
markn92 committed
        xml.sax.parse(osm_xml_data, OSMHandler)
markn92's avatar
markn92 committed

        self.nodes = nodes
        self.ways = ways

        # count times each node is used
        node_histogram = dict.fromkeys(self.nodes.keys(), 0)
        for way in self.ways.values():
            if len(way.nds) < 2:  # if a way has only one node, delete it out of the osm collection
                del self.ways[way.id]
            else:
                for node in way.nds:
                    node_histogram[node] += 1

        # use that histogram to split all ways, replacing the member set of ways
        new_ways = {}
        for id, way in self.ways.items():
            split_ways = way.split(node_histogram)
            for split_way in split_ways:
                new_ways[split_way.id] = split_way
        self.ways = new_ways