Routing mit einem dichten Punktnetz

Posted on Sa 13 Februar 2021 in Blog

Zum Thema Routing mit Daten der OpenStreetMap gab es in letzter Zeit schon zwei Posts:

Bei beiden war das "Problem", dass zwischen den Punkten interpoliert wurde und es so aussieht, als ob jeder Punkt in Österreich mit dem Auto erreichbar wäre. Was ja nicht ganz den Tatsachen entspricht. Die Inspiration für den nächsten Versuch habe ich mir über das QGIS-Plugin QNEAT3 geholt.

Inhalt

Idee

Als Basis werden die Daten der OpenStreetMap und die Klassifikation des Urban Audit (2020) von Eurostat dienen. Im ersten Schritt werden die entsprechenden Straßentypen aus der OpenStreetMap ausgewählt, die für ein Routing mittels Auto in Frage kommen. Bei den Straßen handelt es sich um Multiline-Objekte. Daher kommt die Idee, die einzelnen Punkte für das Routing heranzuziehen. So entsteht ein sehr feines Netz in Regionen mit hoher Straßendichte, während es im alpinen Raum spärlicher sein wird.

Vorbereitungen

Als Daten dienen die Daten der OpenStreetMap, die bei mir im Schema at_osm gespeichert sind und mittels osm2pgsql eingespielt wurden. Die Daten des Urban Audit liegen im Schema at_routing. Sind die Punktdaten von Eurostat, die dann nur die österreichischen Orte umfassen.

SQL

Im ersten Schritt werden alle Straßen, die für das Routing in Frage kommen aus den Daten der OpenStreetMap extrahiert und mittels lateral join die einzelnen Punkte mittels ST_DumpPoints extrahiert. Danach werden alle extrahierten Punken mit allen Städten des Urban Audit verknüpft. Das Resultat der Abfrage wird auch gleich in einer Tabelle gespeichert. Disclaimer: aus Gründen der Faulheit und Einfachheit habe ich auch die Punkte dazugespeichert. Ist nicht unbedingt der ideale Ansatz, aber bei der überschaubaren Datenmenge ist es nicht das große Problem

create table at_routing.urban_edges as
with select_osm_points as (
    select osm_id, x.geom from 
    at_osm.planet_osm_line left join lateral ST_DumpPoints(st_points(way)) as x on true 
    where highway in ('motorway', 'trunk', 'primary', 'secondary', 'tertiary', 'unclassified', 'residential', 
    'motorway_link', 'trunk_link', 'primary_link', 'secondary_link', 'tertiary_link', 'living_street')
)
select id, b.geom, osm_id, a.geom as target_geom from select_osm_points as a, at_routing.cities_at_urban_audit as b;

Im nächsten Schritt wird noch eine routing_id erstellt

alter table at_routing.urban_edges add column routing_id int GENERATED BY DEFAULT AS IDENTITY;

Danach wird die Tabelle für die Resultate des Routings erstellt:

create table at_routing.urban_routes (
    routing_id int,
    distance numeric,
    duration numeric
);

Für das Routing werde ich wieder eine Kombination aus OSRM und dem PostgreSQL HTTP Client verwenden. Eine genauere Anleitung findet sich im Beitrag Routing von OSM-Daten mit OSRM und Postgres. Die Abfrage selbst bietet wieder wenig Überraschungen

insert into at_routing.urban_routes
with select_routes as (
    select routing_id, ST_X(ST_Transform(geom, 4326)) || ',' || ST_Y(ST_Transform(geom, 4326)) || ';' || ST_X(ST_Transform(target_geom, 4326)) || ',' || ST_Y(ST_Transform(target_geom, 4326)) as route
from at_routing.urban_edges
)
select routing_id, (content::json->'routes'->0->>'distance')::numeric as distance, (content::json->'routes'->0->>'duration')::numeric as duration
from select_routes left join lateral http_get('http://127.0.0.1:5000/route/v1/driving/' || route || '?steps=false&overview=simplified&annotations=false') as b on true;

Prinzipiell lässt sich das Routing auch wieder mit einem simplen Python-Script parallelisieren. Siehe dazu auch Naive räumliche Interpolation in Postgres. Nach erfolgreichem Routing geht es an die Auswertung der Daten. Hierfür werden die Straßen wieder in einzelne Punkte zerlegt und mit den Resultaten verknüpft. Danach werden die einzelnen Punkte wieder zu Linien zusammengefügt. Jede Wegstrecke bekommt die Summe der zwei Punkte, die dann durch zwei dividiert wird, als Fahrzeit zugerechnet. Ist ein etwas simpler Ansatz, aber für einen ersten Test durchaus ausreichend. Das ganze speichere ich dann als materialized view.

create materialized view at_osm.v_routing_street_results as
with select_osm_points as (
    select osm_id, x.geom, x.path[1] as path_number from 
    at_osm.planet_osm_line left join lateral ST_DumpPoints(st_points(way)) as x on true 
    where highway in ('motorway', 'trunk', 'primary', 'secondary', 'tertiary', 'unclassified', 'residential', 
    'motorway_link', 'trunk_link', 'primary_link', 'secondary_link', 'tertiary_link', 'living_street')
),
get_min_duration as (
    select osm_id, target_geom, min(duration) / 60 as min_duration from at_routing.urban_routes as a, at_routing.urban_edges as b 
    where a.routing_id=b.routing_id
    group by 1,2
),
get_min_distance_per_point as (
    select a.osm_id, geom, path_number, min_duration from select_osm_points as a inner join 
    get_min_duration as b on (a.osm_id=b.osm_id and geom=target_geom)
)
select a.osm_id, ST_MakeLine(a.geom, b.geom) as geom, (a.min_duration + b.min_duration) / 2.0 as duration from 
get_min_distance_per_point as a inner join get_min_distance_per_point as b on (a.osm_id=b.osm_id and a.path_number+1=b.path_number)

Zusammenfassung

Im Unterschied zu Daten, die interpoliert werden sieht man am Beispiel, der hier dargestellten Erreichbarkeiten, schön die Topografie und die damit einhergehenden Herausforderungen. Während es im außeralpinen Raum ein dichtes Verkehrsnetz gibt, gibt es inneralpin naturgemäß weniger Verkehrswege. Ob weniger Verkehrswege automatisch etwas Schlechtes sind, muss jede(r) für sich selbst entscheiden (Stichwort Flächenversiegelung). Hier soll es einfach darum gehen die Möglichkeiten im Zusammenspiel mit Daten der OpenStreetMap und Postgres bzw. QGIS zu zeigen.

Detailierte Erreichbarkeiten für Österreich