User:Brogo/Kartenraster und Straßenverzeichnis
Erstellung eines Kartenrasters und Straßenverzeichnisses
ACHTUNG: Die Beschreibung ist aus dem Jahr 2013. Da sich die Software mittlerweile geändert hat, kann es sein, dass die Beispiele/Lösungen so nicht mehr funktionieren. Sie bleiben aber als Anregung erhalten. |
Intention
Ich wollte für gedruckte Karte meiner Heimatgemeinde erstellen und diese mit einen Straßenverzeichnis und dazugehörigen Kartenraster versehen.
Vorgehensweise
Zunächst erstellen wir ein Raster, welches sowohl für den späteren Ausdruck benutzt wird, als auch für die Zuordnung der Straße dient. Dazu wird das Raster als OSM-Datei in eine Datenbank geladen und mittels einer Abfrage wird ermittelt, durch welches Kästchen eine Straße läuft.
Erstellen des Rasters
Bei meinen Recherchen nach fertigen Lösungen bin ich auf https://github.com/k4r573n/theGrid/blob/master/grid.php von Karsten Hinz gestossen. Das Ganze sah schon sehr gut aus. Allerdings hat sein Script nur Linien gezeichnet. Ich wollte ja geschlossene Kästchen, damit ich abfragen kann, ob eine Straße durch ein bestimmtes Kästchen führt.
Ich kannte mich mit PHP praktisch kaum aus. Aber ich konnte Karstens Vorlage recht einfach umbauen.
Durch Aufruf der PHP-Datei auf einem Webserver erhält man als Antwort eine OSM-Datei mit dem Gitter. Die Variablen sind am Anfang der Datei definiert. Alternativ kann man dem Skript mit z.B. gen_grid.php?mbbox=[bbox=10.6,53.975,10.78,54.065] eine Bounding Box in der Form [left,bottom,right,top] übergeben.
PHP-Datei
<?php
//Liefert ein Kartenraster in Form von geschlossenen OSM-Ways
//inklusive Rahmen und Beschriftungen
//Koordinaten können über den Paramet mbbox übergeben werden
//ACHTUNG! Die erzeugte OSM-Datei NICHT hochladen!
//Basiert auf https://github.com/k4r573n/theGrid/blob/master/grid.php von Karsten Hinz
//Die einzelnen Kästchen erhalten folgende Tags: 'grid=line` und 'name=*' als "Koordinate" also z.B. A1, B2 usw.
//Die Rahmenlinien erhalten 'grid=border'
//Die Achsenbeschriftungen sind 'grid=letter' bzw. 'grid=number' und dem jeweiligen Wert in 'name=*'
//Definition Variablen bzw. Vorbelegung
$user="brogos_php_grid";
$time="2012-11-06T11:32:24Z";
$left = 10.6;
$right = 11.2;
$top = 54.065;
$bottom = 53.975;
$node_id=-1000000000;
$way_id=-2000000000;
$rahmen_node_id=-3000000;
$rahmen_way_id=-4000000;
$x=0;
$y=0;
$node_1=0;
$node_2=0;
$node_3=0;
$node_4=0;
$tags="";
$number="";
$zaehler=1;
$buchstabe="";
$zusatzbuchstabe="";
//Übernahme des Parameters mbbox
if (isset($_GET['mbbox'])) {
$bbox = substr($_GET['mbbox'], 6, strlen($_GET['mbbox'])-7);
list($left, $bottom, $right, $top) = explode(",", $bbox);
}
//Zerlegung des Parameters mbbox in die einzelnen Werte
if (isset($_GET['left']))
$left = $_GET['left'];
if (isset($_GET['right']))
$right = $_GET['right'];
if (isset($_GET['top']))
$top = $_GET['top'];
if (isset($_GET['bottom']))
$bottom = $_GET['bottom'];
// Header schreiben
print "<?xml version='1.0' encoding='UTF-8'?>\n";
print "<osm version='0.6' generator='php-grid-generator' upload='false'> \n";
print "<bounds minlat='$bottom' minlon='$left' maxlat='$top' maxlon='$right' origin='generated' />\n";
//Vorgabe für den Abstand der Kästchen, Abstand 0,5 km.
//delta_y ist für alle Positionen auf der Erde gleich (Abstand zwischen den Breitengraden ca. 111 km).
//Um delta_x zu errechnen, benötigt man den Breitengrad (bg), denn trägt man in die folgende Formel ein:
//=0,5/(2*PI()*6371*COS(bg*PI()/180)/360);
//ansonsten gibt es je nach Breitengrad keine Quadrate, sondern Rechtecke.
$delta_x = 0.0076500;
$delta_y = 0.0045045;
//Berechnung der Anzahl der Kästchen, Rundung auf Ganzzahl, Berechnung des daraus resultierenden Kästchenbreite
$anz_x= round(($right - $left)/$delta_x,0) ;
$anz_y= round(($top - $bottom)/$delta_y,0) ;
$delta_x = ($right - $left)/($anz_x + 1/3);
$delta_y = ($top - $bottom)/($anz_y + 1/3);
//links-oben-Ecke des Gitters berechnen
$top_gitter=$top - $delta_y/3;
$left_gitter=$left + $delta_x/3;
// Nodes schreiben
for ($m = 1; $m < $anz_y+2; $m++ ) {
for ($n = 1; $n < $anz_x+2; $n++, $node_id--) {
$y = $top_gitter - ($m-1) * $delta_y;
$x = $left_gitter + ($n-1) * $delta_x;
$node_id = -$n - $m*1000000000 ;
print "\t<node id='$node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$y' lon='$x'>\n".
"\t</node>\n";
}
}
// Ways schreiben
for ($m = 1; $m < $anz_y+1; $m++ )
{$zaehler=1;
$zusatzbuchstabe="";
for ($n = 1; $n < $anz_x+1; $n++, $way_id--, $zaehler++) {
$node_1= (-$n-0 - ($m+0)*1000000000);
$node_2= (-$n-1 - ($m+0)*1000000000);
$node_3= (-$n-1 - ($m+1)*1000000000);
$node_4= (-$n-0 - ($m+1)*1000000000);
if ($zaehler > 26) {
$zaehler = $zaehler - 26;
$buchstabe = $zaehler ;
if (empty($zusatzbuchstabe)) {
$zusatzbuchstabe="A";
}
else {
$zusatzbuchstabe=chr(ord($zusatzbuchstabe)+1);
}
}
else {
$buchstabe = $zaehler;
}
$number=$zusatzbuchstabe.chr($buchstabe+64).$m;
print "\t<way id='".$way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='name' v='".$number."' />\n".
"\t\t<tag k='grid' v='line' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t\t<nd ref='".$node_3."' />\n".
"\t\t<nd ref='".$node_4."' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t</way>\n";
}
}
//Nodes für die Rahmenlinien setzen
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top' lon='$left'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top' lon='$left_gitter'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top' lon='$right'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top_gitter' lon='$left'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top_gitter' lon='$left_gitter'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$top_gitter' lon='$right'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$bottom' lon='$left'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$bottom' lon='$left_gitter'>\n".
"\t</node>\n";
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$bottom' lon='$right'>\n".
"\t</node>\n";
//Rahmenlinien zeichnen
$node_1=$rahmen_node_id+8;
$node_2=$rahmen_node_id+6;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
$rahmen_way_id=$rahmen_way_id-1;
$node_1=$rahmen_node_id+5;
$node_2=$rahmen_node_id+3;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
$rahmen_way_id=$rahmen_way_id-1;
$node_1=$rahmen_node_id+2;
$node_2=$rahmen_node_id;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
$rahmen_way_id=$rahmen_way_id-1;
$node_1=$rahmen_node_id+8;
$node_2=$rahmen_node_id+2;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
$rahmen_way_id=$rahmen_way_id-1;
$node_1=$rahmen_node_id+7;
$node_2=$rahmen_node_id+1;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
$rahmen_way_id=$rahmen_way_id-1;
$node_1=$rahmen_node_id+6;
$node_2=$rahmen_node_id;
print "\t<way id='".$rahmen_way_id."' timestamp='$time' user='$user' visible='true' version='1'>\n".
"\t\t<tag k='grid' v='border' />\n".
"\t\t<nd ref='".$node_1."' />\n".
"\t\t<nd ref='".$node_2."' />\n".
"\t</way>\n";
//Beschriftung (A, B, C, [...], Y, Z, AA, AB, ...) X-Achse als Nodes setzen
$zaehler=1;
$zusatzbuchstabe= NULL;
$x=$left_gitter + $delta_x/2;
$y=$top_gitter + $delta_y/6;
for ($m = 0; $m < $anz_x; $m++, $zaehler++ ) {
if ($zaehler > 26) {
$zaehler = $zaehler - 26;
$buchstabe = $zaehler ;
if (empty($zusatzbuchstabe)) {
$zusatzbuchstabe="A";
}
else {
$zusatzbuchstabe=chr(ord($zusatzbuchstabe)+1);
}
}
else {
$buchstabe = $zaehler;
}
$number=$zusatzbuchstabe.chr($buchstabe+64);
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$y' lon='$x'>\n".
"\t\t<tag k='name' v='".$number."' />\n".
"\t\t<tag k='grid' v='letter' />\n".
"\t</node>\n";
$x = $x + $delta_x;
}
//Beschriftung (1, 2, 3, ...) Y-Achse als Nodes setzen
$x=$left_gitter - $delta_x/6;
$y=$top_gitter - $delta_y/2;
for ($n = 1; $n < $anz_y+1; $n++ ) {
$rahmen_node_id=$rahmen_node_id-1;
print "\t<node id='$rahmen_node_id' timestamp='$time' user='$user' visible='true' version='1' lat='$y' lon='$x'>\n".
"\t\t<tag k='name' v='".$n."' />\n".
"\t\t<tag k='grid' v='number' />\n".
"\t</node>\n";
$y = $y - $delta_y;
}
//XML-Ausgabe schliessen
print "</osm>\n";
?>
ACHTUNG: Die erstellte OSM-Datei NICHT zu den OSM-Servern hochladen. Datei NUR LOKAL verwenden.
Datenbankabfrage
Datenbank erstellen und füllen
Ich habe eine OSM-Datenbank mit osmosis-snapshot-Schema mit bbox und linestring gewählt. Zur Installtion und Import siehe Osmosis/PostGIS_Setup.
Ich erläutere hier nur einige Punkte zur PostGIS, die mir besonders aufgefallen sind. Auf ALLE Einzelheiten zur Datenbank gehe ich hier nicht ein, das wäre an dieser Stelle dann zu umfangreich.
Als Windows-User bevorzuge ich die Nutzung von Postgres über die Software PGAdmin.
Zunächst importiere ich die Datei mit dem Gitter. Dann erstelle ich folgenden SQl-Befehle eine weitere Tabelle.
CREATE TABLE grid
(
id bigint NOT NULL,
version integer NOT NULL,
user_id integer NOT NULL,
tstamp timestamp without time zone NOT NULL,
changeset_id bigint NOT NULL,
tags hstore,
nodes bigint[],
bbox geometry(Geometry,4326),
linestring geometry(Geometry,4326),
CONSTRAINT pk_grid PRIMARY KEY (id )
)
WITH (
OIDS=FALSE
);
ALTER TABLE grid
OWNER TO postgres;
CREATE INDEX idx_grid_bbox
ON grid
USING gist
(bbox );
CREATE INDEX idx_grid_linestring
ON grid
USING gist
(linestring );
Dann erstelle ich Abfrage nach meinem Key 'grid' und kopiere das Ergebnis in die neue Tabelle.
INSERT into grid
SELECT *
FROM ways w
WHERE w.tags::hstore ? 'grid' ;
Erste Abfrage
Mit
SELECT DISTINCT w.tags::hstore -> 'name' as strassenname, g.tags::hstore -> 'name' as feld
FROM ways w, grid g
WHERE ST_CENTROID(w.linestring) && g.linestring AND (w.tags::hstore ? 'name' ) ;
kann man jetzt abfragen in welchem Kästchen eine Straße liegt. Die PostGIS-Fuktion ST_CENTROID liefert einen Mittelpunkt des Weges, der aber nicht unbedingt auch auf dem Weg liegt. "&&" ist eine PostGIS-Funktion welche abfragt, ob sich die Bounding-Boxen zweier Objekte überschneiden. Da wir mit rechteckigen Kästchen arbeiten, ist das völlig OK.
Abfrage mit Gemeindegrenze
Nun habe ich die Abfrage dahingehend ergänzt, daß ich nur Straßen abfrage, die innerhalb der Gemeindegrenzen liegen. Dafür frage ich die Relations-ID für meine Gemeinde ab und baue mit der Zeile "(select ST_Multi(..." die einzelnen Linien zu einem Polygon zusammen.
SELECT DISTINCT w.tags::hstore -> 'name' as strassenname, w.tags::hstore -> 'highway' as typ, g.tags::hstore -> 'name' as feld
FROM ways w, grid g
WHERE (w.linestring) && g.linestring AND (w.tags::hstore ? 'highway' ) AND (w.tags::hstore ? 'name' )
AND ST_Intersects(
(select ST_Multi(ST_BuildArea(ST_Union(linestring))) geom
from (
SELECT w.linestring
from relations r, relation_members rm, ways w
WHERE r.id = 382443
AND r.id = rm.relation_id
AND rm.member_id = w.id
) www ),w.linestring)
order by strassenname, feld
;
Postleitzahlen
Um noch die Postleitzahl auszuwerten muß ich zunächst die Relationen der PLZ-Gebiete zusammensetzen. Irgendwie habe ich es nur über zwei Sichten hinbekommen. Ist vielleicht auch übersichtlicher, als alles in eine einzige Abfrage zu packen.
Die erste Sicht sucht alle Linen zu PLZ-Relation raus.
CREATE OR REPLACE VIEW plz AS
SELECT w.linestring, r.tags -> 'postal_code'::text AS plz_nr
FROM relations r
JOIN relation_members rm ON r.id = rm.relation_id
JOIN ways w ON rm.member_id = w.id
WHERE r.tags ? 'postal_code'::text;
ALTER TABLE plz
OWNER TO postgres;
Die zweite Sicht baut diese Linien dann zu einem Polygon zusammen.
CREATE OR REPLACE VIEW plz_areas AS
SELECT st_multi(st_buildarea(st_union(plz.linestring))) AS plz_geom, plz.plz_nr
FROM plz
GROUP BY plz.plz_nr;
ALTER TABLE plz_areas
OWNER TO postgres;
Komplette Abfrage
Nun kann ich mit einer Abfrage alle Straßen innerhalb einer Gemeinde mit ihrem Namen, Straßentyp, Kästchen und Postleitzahl abfragen.
SELECT DISTINCT w.tags::hstore -> 'name' as strassenname, w.tags::hstore
-> 'highway' as typ, g.tags::hstore -> 'name' as feld, pa.plz_nr
FROM ways w, grid g, plz_areas pa
WHERE (w.linestring) && g.linestring AND (w.tags::hstore ? 'highway' )
AND (w.tags::hstore ? 'name' )
AND ST_Intersects(
(select ST_Multi(ST_BuildArea(ST_Union(linestring))) geom
from (SELECT w.linestring
FROM relations r
JOIN relation_members rm on r.id = rm.relation_id
JOIN ways w on rm.member_id = w.id
WHERE r.id = 382443
ORDER BY rm.sequence_id
) www ),w.linestring)
AND ST_Intersects
(pa.plz_geom,w.linestring)
order by strassenname, feld
;
Das Ergebnis sieht dann etwa so aus:
"Alte Bergstraße";"residential";"V9";"23683" "Alte Bergstraße";"residential";"V8";"23683" "Ahrensböker Straße";"unclassified";"J5";"23684" "Ahrensböker Straße";"unclassified";"I5";"23684" "Ahornweg";"residential";"W11";"23683" "Ahornweg";"residential";"W10";"23683" "Agnes-Miegel-Weg";"residential";"K6";"23684" "Agnes-Miegel-Weg";"residential";"K5";"23684" "Aalweg";"residential";"U4";"23683" "Aalweg";"residential";"U3";"23683" "Aalweg";"residential";"T4";"23683" "Aalweg";"residential";"T3";"23683"
Wie man sieht, muß man hier noch manuell nacharbeiten und mehrfache Kästchen zusammenfassen.