Landshut/Projekte Rendering

From OpenStreetMap Wiki
Jump to navigation Jump to search

Rendering Projekte für Printmedien

Zurück zur Landshut Wiki Seite

Zurück zur Landshut Wiki Projekte Seite

(Gernot hat da so ein schönes Template verwendet, aber dafür bin ich zu blöd...)

Relationen (hier Wanderwege) hervorheben

Folgendes Beispielfile im inc/layer-hiking-routes.xml.inc anlegen, und im osm.xml mit &layer-hiking-routes; includen:

 <Style name="hiking-routes">
     <Rule>
       &maxscale_zoom7;
       &minscale_zoom14;
       <LineSymbolizer stroke-linejoin="round" stroke-linecap="round"   stroke="#ff0000ff" stroke-width="4"/>
     </Rule>
     <Rule>
      &maxscale_zoom15;
       <LineSymbolizer stroke-linejoin="round" stroke-linecap="round"  stroke="#ff0000ff" stroke-width="20"/>
     </Rule>
 </Style>
 
 <Style name="hiking-labels">
     <Rule>
       <ShieldSymbolizer size="20" fill="#fff" placement="line" file="&symbols;/hike.svg" type="svg" spacing="800" minimum-distance="40" fontset-name="bold-fonts">[ref]</ShieldSymbolizer>
     </Rule>
 </Style>
 
 
 <Layer name="hiking-routes" status="on" srs="&osm2pgsql_projection;">
     <StyleName>hiking-routes</StyleName>
     <StyleName>hiking-labels</StyleName>
     <Datasource>
       <Parameter name="table">
       (select way,ref from &prefix;_line where route='hiking' ) as routes
       </Parameter>
       &datasource-settings;
     </Datasource>
 </Layer>

Tricky: die Einbahnstraßensymbole

Bei der Umstellung der Icons von .png auf .svg wollte ich auch die Einbahnstraßensymbole vekorisieren. Da gibt es z.B. ein arrow.png, aber die Umstellung auf .svg hat nichts bewirkt, der Pfeil war weiterhin verpixelt. Tobi hat dann herausgefunden, dass die Einbahnstraßenpfeile mit einem sehr fießen Trick gerendert werden. Und zwar im osm.xml:

   <Rule>
     <Filter>[oneway] = 'yes'</Filter>
     &maxscale_zoom16;
     <LineSymbolizer stroke-linejoin="bevel" stroke="#6c70d5" stroke-width="1" stroke-dasharray="0,12,10,152"/>
     <LineSymbolizer stroke-linejoin="bevel" stroke="#6c70d5" stroke-width="2" stroke-dasharray="0,12,9,153"/>
     <LineSymbolizer stroke-linejoin="bevel" stroke="#6c70d5" stroke-width="3" stroke-dasharray="0,18,2,154"/>
     <LineSymbolizer stroke-linejoin="bevel" stroke="#6c70d5" stroke-width="4" stroke-dasharray="0,18,1,155"/>
   </Rule>

Damit wird ein langer Strich, und am Ende drei kurze quer dazu mit kürzer werdenden Abständen erzeugt.


Die Includes

Im osm.xml gibt es etliche sog. Entity-Referenzen, die haben folgende Form:

 &layer-amenity-points;

Im Verzeichnis 'inc' gibt es eine Datei layer-amenity-points.xml.inc. Damit Mapnik diese Datei findet, gibt es im 'inc' Verzeichnis noch ein layers.xml.inc, das beinhaltet u.a.:

 <!ENTITY layer-amenity-points SYSTEM "layer-amenity-points.xml.inc">

Selektive Anzeige von Karten-Elementen

Für unsere Landshuter Stadtkarte haben wir alle Namen von Läden entfernt, weil das sonst zu viel Platz wegnimmt. Jetzt sieht die Karte fast zu leer aus. Eine Idee war, einige handausgewählte (traditionelle) Geschäfte doch mit Namen zu versehen, als Orientierungspunkt sozusagen. Ein weiteres Highlight ist das 'Branding' von Karten, d.h. ein Geschäft bekommt sein eigenes Logo auf die Karte. All das lässt sich mit den mapnik stylesheets erschlagen. Im osm.xml findet sich dann in etwa folgender Text:

<Style name="LA_text">

  <Rule>
    &maxscale_zoom17;
    <Filter>[name]='Bücher Pustet'</Filter>
    <!-- dy=15 brauchen wir, weil sonst Text und Icon um den Platz kaempfen -->
    <TextSymbolizer size="10" fill="#000000" dy="15" fontset-name="book-fonts" halo-radius="0" wrap-width="10" placement="interior">[name]</TextSymbolizer>
    <PointSymbolizer file="&symbols;/branding_pustet.svg" type="svg" placement="interior"/>
  </Rule>

  <Rule>
    &maxscale_zoom17;
    <TextSymbolizer size="10" fill="#000000" dy="9" fontset-name="book-fonts" halo-radius="0" wrap-width="10" placement="interior">[name]</TextSymbolizer>
  </Rule>

</Style>

Für den Style. Wo im osm.xml der Style steht, ist nicht so wichtig. Die Position des Layers ist da eher entscheidend, ich habe den neuen Layer an folgender markanten Stelle eingefügt (&layer-admin; ist die erste originale Codezeile):

<Layer name="LA_text" status="on" srs="&osm2pgsql_projection;">
    <StyleName>LA_text</StyleName>
    <Datasource>
      <Parameter name="table">
      (select way,amenity,shop,access,leisure,landuse,man_made,"natural",place,tourism,ele,name,ref,military,aeroway,waterway,historic,'yes'::text as point
       from &prefix;_point
       where shop in
        ('books','stationery','supermarket','photography','bakery','clothes','fashion','convenience','doityourself','hairdresser','department_store','butcher',
         'car','car_repair','bicycle','florist') and 
       name in ('Hugendubel', 'Bücher Pustet', 'Oberpaur', 'Büro Dallmer')
      ) as LA_text
      </Parameter>
      &datasource-settings;
    </Datasource>
</Layer>

&layer-admin;
&layer-placenames;
&layer-amenity-stations;
&layer-amenity-symbols;

Die Layer Sektion filtert nur die Geschäfte heraus, die wir hervorheben wollen. Deren Namen (!) werden dann mit dem o.g. Style gezeichnet. Ob ein Geschäft (shop=car) ein Icon bekommt, wird im inc/layer-amenity-points.xml.inc festgelegt. Der "Bücher Pustet" bekommt ein eigenes Icon. Weil das etwas größer ist als die Standard-Icons, müssen wir noch die Beschriftung etwas weiter unten als üblich hinzeichnen. Ob die Rules sich gegenseitig ausschliessen oder auch gemeinsam auf ein Objekt angewewndet werden, weiss ich derzeit noch nicht.

Webfrontend zum Erstellen von .svg Grafiken eines ausgewählten Bereichs

Die Idee war, über eine slippy map (OpenLayers) eine Möglichkeit anzubieten, zu einem bestimmten Gebiet heranzuzoomen und dieses dann on-the-fly als .svg rendern zu lassen. Die Benutzung des generate_image.py Skripts ist ja schon an anderen Stellen halbwegs beschrieben. Das größte Problem dabei ist, die Koordinaten auf der Weltkarte (lon/lat) herauszufinden und das entsprechende Ausgabeformat (DIN-Ax) mit einer passenden Aufloesung zu berechnen. Das ist viel try-and-error mit Taschenrechner und Gemurkse im Skript.

Slippy Map einrichten

Dazu braucht man natürlich erstmal einen eigenen Web-Server, auf dem man ohne Sorge rumpfuschen kann.

Beispiele für slippy maps gibt's genug. Die Stolpersteine waren hauptsächlich das ständige Umrechnen von Koordinaten mit Transformationen, so dass die Werte immer so rauskommen, wie man sie von OSM gewohnt ist (lon/lat als Grad). OpenLayers ist nicht nur für OSM gebaut und arbeitet deshalb intern mit einer anderen Projektion, nämlich EPSG:4326, währen die OSM Koordinaten EPSG:3857 verwenden.

   map.projection = "EPSG:3857";
   map.displayProjection = new OpenLayers.Projection("EPSG:3857");

Um die Maus-Koordinaten rechts unten auch wie gewohnt darzustellen:

   map.addControl(new OpenLayers.Control.MousePosition( {id: "utm_mouse", prefix: "", displayProjection: map.displayProjection, numDigits: 6} ));

Die slippy map Karte in der Website habe ich gleich so dimensioniert, dass sie einem DIN-A* Format entspricht - fällt schon mal das Beachten des Seitenverhältnis weg:

  <div id="mapdiv" style="width:630px; height:891"></div>

Weil das Rein/Rauszoomen der Karte immer gleich sehr große Sprünge macht, kann man mit zwei zusätzlichen Buttons die Größe des div's in kleinen Schritten verändern, um den Ausschnitt noch exakter zu bestimmen. Das geht mit ein wenig JavaScript Code:

 <input type="button" value="+" onclick='enlarge()'/>

und

 function enlarge()
 {
   // den mapdiv um 5% vergroessern
   var div_width = document.getElementById( "mapdiv" ).style.width; // "630px"
   div_width = div_width.substr( 0, div_width.length - 2 );
   div_width = div_width * 1.05;
   document.getElementById( "mapdiv" ).style.width = div_width + "px";
   ...
   updateBBoxLabel();
   ...
   map.updateSize();

usw... Zum updateBBoxLabel später.

Das map.updateSize() ist unbedingt notwendig, wenn man die Größe des div's geändert hat. das map Objekt bekommt diese Änderung nämlich nicht mit und liefert im Anschluß unbrauchbare Werte!

Hat man den Ausschnitt wie gewünscht gewählt, müssen nun die Koordinaten des Rechtecks irgendwie an den Renderer weitergeleitet werden. Das habe ich ganz profan mit dem HTML 'form' Tag und CGI-Script gelöst:

 <form action="cgi-bin/create_svg.cgi" method="post">
 <input type="text" name="bbox" id="bbox" size="50"/>
 <input type="submit" value="Create SVG now!">

Das Textfeld mit dem Namen (nicht Id!) 'bbox' wird per stdin an das Skript weitergeleitet, sobald man den Submit Button drückt. Der Inhalt des Textfeldes wird über onclick Events (der +/- Buttons), sowie zoom/move Events des map Objektes aktualisiert.

 function updateBBoxLabel()
 {
   // Die intern verwendete EPSG:4326 Transformation in was Brauchbares umwandeln
   var map_bounds = map.getExtent();
   map_bounds.transform( map.getProjectionObject(),
                         new OpenLayers.Projection("EPSG:3857") );
   document.getElementById( "bbox" ).value = map_bounds.toBBOX();
 }

Und dann z.B.:

   map.events.on({"zoomend": updateBBoxLabel, "scope": map});
   map.events.on({"moveend": updateBBoxLabel, "scope": map});

CGI einrichten

Damit der HTTP-Server mit der cgi-Direktive im form-Tag auch etwas anfangen kann, muss man die Konfigurationsdatei anpassen. Irgendwo unter /etc/apache2/apache2.conf:

 <Directory /var/www/poster-frontend>
     Options Indexes FollowSymlinks +ExecCGI
     AddHandler cgi-script cgi
 </Directory>
 # fuehrende und schliessende Slashes sind unbedingt notwendig!
 ScriptAlias /poster-frontend/cgi-bin/ /var/www/poster-frontend/cgi-bin/

Und die Konfiguration neu laden mit '/etc/init.d/apache2 reload'. Fehlerausgaben beachten. Klappt's mit dem CGI Zeug nicht, hilft auch die Log-Ausgabe des apache unter /var/log/apache2/error.log oft weiter.

Man muss sich an dieser Stelle schon bewusst sein, dass hier jede Menge Sicherheitslöcher aufgerissen werden, und das nicht mehr state-of-the-art Webprogrammierung ist!

Das im Web-Formular hinterlegte create_svg.cgi liegt im var/www/poster-frontend/cgi-bin/, und die ScriptAlias Direktive für den apache generiert die entsprechende Verbindung dahin. Damit ist das CGI scharf geschaltet.

Wird das create_image.cgi aufgerufen, erhält es die Inhalte der Formularfelder über den Standard-Input. Das muss dann entsprechend geparsed werden um die einzelnen Werte der Koordinaten so aufzubereiten, dass man sie schließlich dem generate_image.py übergeben kann.

Ein CGI-Skript muss immer einen gültigen HTTP-Content (also keine HTML-Seite!) zurückliefern - siehe zweite Zeile. Hier das komplette Skript:

create_svg.cgi:

 #!/bin/sh
 printf "Content-type: text/html\r\n\r\n"
 
 # stdin liefert:
 # bbox=-73.171094%2C-32.779472%2C37.571094%2C76.410991
 python_args=`
 cat - |
 	# Felder werden durch Ampersand getrennt
 	sed 's/&/ /g' |
 	# Komma:
 	sed 's/%2C/ /g' |
 	sed 's/bbox=//g'
 `
 # jetzt:
 # -73.171094 -32.779472 37.571094 76.410991
 
 cd /home/osm/mapnik-stylesheets
 mv image.png image.png.old
 mv image.svg image.svg.old
 ./generate_image.py $python_args
 
 echo "On success, you can now download here the <a href=../image.svg>image.svg</a>"

Das Skript wird als user 'www-data' ausgeführt. D.h. der muss auch Schreibrechte im Verzeichnis /home/osm/mapnik-stylesheets haben. Das ist natürlich absolut kriminell, aber was soll's...

Kurz noch zum commandline arguments parsing in python:

   # Default:
   bounds = (12.14819,48.53311,12.1563,48.54025)
 
   if len(sys.argv) == 5:
       left=float(sys.argv[1])
       bottom=float(sys.argv[2])
       right=float(sys.argv[3])
       top=float(sys.argv[4])
 
       if left < 12.097:
           print "left border out of range."
           sys.exit(1)

usw, ein paar Sicherheitschecks, damit die Boundingbox nicht zu groß wird...

       bounds = ( left, top, right, bottom )

Das war's! Jetzt hat generate_image.py unseren in der SlippyMap ausgewählten Ausschnitt erhalten und kann mit dem Rendern loslegen!

Das create_svg.cgi erzeugt neben ein paar Ausgaben auch noch einen Hyperlink, all das landet in der Webseite, die der Benutzer sieht, sobald er auf den Submit Button gedrückt hat. Der Link verweist auf ../image.svg. Dies ist ein Symlink (im Filesystem) auf /home/osm/mapnik-stylesheets/image.svg. Das '../' ist deshalb nötig, weil die aktuelle URL nicht mehr /poster-frontend ist, sondern poster-frontend/cgi-bin.

Das Schöne ist, dass die Browser alle SVG unterstützen, d.h. man kann direkt auf den Link klicken und bekommt das gerenderte Bild angezeigt!

Repository

Die aktuellen Skripten sind in SVN eingecheckt. Zum Abholen:

 svn co file:///home/svn-repos/mapnik-city

das 'web' Verzeichnis beinhaltet den interaktiven Kartengenerator, auf dieses muss ein Symlink im /var/www angelegt werden.

Ausgabeformat und Auflösung bestimmen

Derzeit ist im generate_image.py Skript das Zielformat, d.h. Pixel x Pixel, noch fest codiert.

generate_image.py ist in der Lage, sowohl .svg, als auch .png zu erzeugen, wobei die Qualität von Letzterem nicht überzeugend ist. Lieber lädt man ein .svg in inkscape und exportiert das dann.

Die Ausgabe in .svg hat eine feste Auflösung von 'Punkt', das entspricht 1/72 inch.

Die Variablen 'imgx' und 'imgy' legen somit fest, wie fein aufgelöst mapnik einzelne Elemente auf der Karte darstellt. Sind diese Werte klein, verschwinden Details, die Strassen und Namen werden 'klobiger'. Sind die Werte gross, erhält man eine Karte mit unendlich kleiner Schrift und sämtlichen Details, die jenseits von Zoomlevel 17 sind. Es gibt keine festen Zoomlevels, mapnik entscheidet anhand dieser Relation selber, wieviele und welche Elemente dargestellt werden.

Das bietet Raum zum experimentieren, um den gewünschten Detailgrad und die gewünschte Größe der Elemente herauszufinden.