09 August 2015

Das GeoIG resp. die GeoIV sieht Downloaddienste für Geobasisdaten vor. Kurzum heisst das, dass diese Geobasisdaten dienstebasiert und modellkonform zum Download bereitgestellt werden. Modellkonform bedeutet - sehr einfach ausgedrückt - entweder INTERLIS/XTF oder INTERLIS/GML. INTERLIS/GML hat den Vorteil, dass man es, im Gegensatz zu INTERLIS/XTF, sinnvoll mittels WFS bereitstellen kann. Dienstebasiert bedeutet, dass nicht bloss ein einfacher Downloadlink publiziert wird, sondern dass zusätzlich zum Link eine Serviceschicht darüber gestülpt wird.

Gedanken über das modellkonforme Bereitstellen von Geodaten hat sich INSPIRE auch schon gemacht. Als Lösungen für Downloaddienste werden zwei Alternativen vorgeschlagen: WFS und Atom + OpenSearch (AtOS). Informationen zu Atom + OpenSearch findet man vor allem in zwei Masterarbeiten. Bei Atom + OpenSearch können die (Geo)daten dateibasiert und vordefiniert heruntergeladen werden. Um dem Servicegedanken gerecht zu werden und nicht bloss stupide HTTP-Links zu publizieren, muss noch eine Schicht drüber gelegt werden. Diese pflanzt dem Ganzen ein wenig Intelligenz ein.

Setzt man sich mit der modellkonformen Bereitstellung von Geodaten auseinander, darf man zum Schluss kommen, dass die Atom + OpenSearch Lösung einfacher und effizienter für eine GDI umsetzbar ist. Zudem schlägt man zwei Fliegen mit einer Klappe: neben modellkonformen Daten können ebenfalls nicht-modellkonforme Daten und Rasterdaten mittels Atom + OpenSearch bereitgestellt werden können. WFS kann natürlich trotzdem eingesetzt werden. In diesem Fall aber nicht für die modellkonforme Bereitstellung. Die gerade eben diskutierten Datenflüsse lassen sich grob wie folgt darstellen:

GDI Datenbereitstellung

Das Schema zeigt die beiden Schritte (1) Datenumbau und (2) Formatumbau. Der Datenumbau kann z.B. mit einem ETL-Werkzeug oder auch mit SQL-Befehlen durchgeführt werden. Für den Formatumbau eignet sich ja hervorragend ili2pg. Der Formatumbau bei der Variante WFS übernimmt der WFS-Server selbst. Nun könnte man zum Schluss kommen, dass die Variante AtOS weniger aktuell als die WFS-Variante ist: Der WFS greift live auf die Daten des MGDM-Topfs zu wohingegen bei AtOS die Datei z.B. einmal pro Tag mit ili2pg physisch produziert wird. Nun, dem ist nicht so. Eine elegante Lösung ist der Einsatz der ili2pg-Bibliotheken als Servlet. Damit kann der Formatumbau erst beim Aufruf der URL, z.B. http://www.example.com/2583_schoenenwerd.xtf, ausgeführt werden. Als Proof of Concept soll die amtliche Vermessung der Gemeinde Schönenwerd im Datenmodell MOpublic mittels Servlet erzeugt werden.

Als erstes brauchen wir einen Servlet-Container. Anstelle von Java kann man auch - um ein paar Zeichen zu sparen - Groovy einsetzen und sich mit Jetty was zusammenbasteln:

#!/usr/bin/env groovy

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.*
import groovy.servlet.*

@Grab(group='org.eclipse.jetty.aggregate', module='jetty-all', version='9.2.10.v20150310')
def startJetty() {
    def server = new Server(8080)
    def handler = new ServletContextHandler(ServletContextHandler.SESSIONS)
    handler.contextPath = '/'
    handler.resourceBase = '.'
    handler.addServlet(IliExport, '/*')

    server.handler = handler
    server.start()
    server.join()
}

println "Starting Jetty, press Ctrl+C to stop."
startJetty()

Interessant sind wahrscheinlich folgende Zeilen:

Zeile 7: Groovy hat mit Grape ein effizientes Dependency Management. Fehlt die gewünschte Bibliothek auf dem System, wird sie einmalig heruntergeladen und gespeichert.

Zeile 13: Hier teilen wir dem Server mit, dass bei allen Requests das Servlet IliExport aufgerufen werden soll.

Das Formatumbau/Export-Servlet sieht dann wie folgt aus:

@GrabConfig(systemClassLoader=true)
@GrabResolver(name='catais.org', root='http://www.catais.org/maven/repository/release/', m2Compatible='true')
@Grab(group='org.postgresql', module='postgresql', version='9.4-1201-jdbc41')
@Grab(group='ch.interlis', module='ili2c', version='4.5.12')
@Grab(group='ch.interlis', module='ili2pg', version='2.1.4')

import javax.servlet.http.*
import javax.servlet.*
import groovy.servlet.ServletCategory

import ch.ehi.ili2db.base.Ili2db
import ch.ehi.ili2db.base.Ili2dbException
import ch.ehi.ili2db.gui.Config
import ch.ehi.ili2pg.converter.PostgisGeometryConverter
import ch.ehi.sqlgen.generator_impl.jdbc.GeneratorPostgresql

class IliExport extends HttpServlet {
    def application

    void init(ServletConfig config) {
        super.init(config)
        application = config.servletContext
    }

    void doGet(HttpServletRequest request, HttpServletResponse response) {
        def requestedFileName = request.getRequestURI() - '/'

        def mapping = [
        '2583_schoenenwerd.xtf': ['modelname':'MOpublic03_ili2_v13', 'dbschema':'av_mopublic_export'],
        '2583_schoenenwerd.gml': ['modelname':'MOpublic03_ili2_v13', 'dbschema':'av_mopublic_export']
        ]
        def params = mapping.get(requestedFileName)
        def modelName = params['modelname']
        def dbschema = params['dbschema']

        def config = new Config()
        config.setDbhost("localhost")
        config.setDbdatabase("xanadu2")
        config.setDbport("5432")
        config.setDbusr("stefan")
        config.setDbpwd("ziegler12")
        config.setDbschema(dbschema)
        config.setDburl("jdbc:postgresql://localhost:5432/xanadu2")

        config.setModels(modelName);
        config.setModeldir("http://models.geo.admin.ch/");

        config.setGeometryConverter(PostgisGeometryConverter.class.getName())
        config.setDdlGenerator(GeneratorPostgresql.class.getName())
        config.setJdbcDriver("org.postgresql.Driver")

        config.setNameOptimization("topic")
        config.setMaxSqlNameLength("60")
        config.setStrokeArcs("enable")
        config.setSqlNull("enable");
        config.setValue("ch.ehi.sqlgen.createGeomIndex", "True");

        config.setDefaultSrsAuthority("EPSG")
        config.setDefaultSrsCode("21781")

        def tmpDir = System.getProperty('java.io.tmpdir')
        def fileName = tmpDir + File.separator + requestedFileName

        config.setXtffile(fileName)

        Ili2db.runExport(config, "")

        ServletOutputStream os = response.getOutputStream();
        FileInputStream fis = new FileInputStream(fileName);
        try {
            int buffSize = 1024;
            byte[] buffer = new byte[buffSize];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                os.write(buffer, 0, len);
                os.flush();
                response.flushBuffer();
            }
        } finally {
            os.close();
        }
    }
}

Zeilen 1 -2: Weil JDBC-Treiber anders geladen werden, müssen wir Grape speziell konfigurieren. Brauchen wir spezielle Mavenrepositories müssen wir diese ebenfalls angeben (Zeile 2).

Zeilen 26 - 34: Sämtliche Requests werden auf dieses Servlet umgeleitet (siehe oben). Der URL-Aufruf entspricht ja eigentlich einem Download einer statischen Datei (INTERLIS/XTF-Datei der Gemeinde Schönenwerd), dh. der Request sieht in unserem Fall wie folgt aus: http://localhost:8080/2583_schoenenwerd.xtf. Die dazugehörigen Daten liegen in der Datenbank in einem Schema. In welchem Schema steht aber nicht in der URL, sondern das müssen wir mittels Mapping herausfinden. In diesem einfachen Proof of Concept stehen die benötigten Informationen in einer Map. Anstelle der Map ist natürlich auch eine Meta-DB o.ä. vorstellbar. Neben des Speicherortes (Datenbankschema) müssen wir noch den Interlis-Modellnamen kennen.

Zeilen 36 - 66: Die INTERLIS/XTF-Datei wird erzeugt und in ein temporäres Verzeichnis geschrieben (= Formatumbau).

Zeilen 68 - 81: Die gerade eben erzeugte Datei wird an den Klienten gestreamt.

Um den Browser nicht zu arg zu belasten (bei 25 MB XML) verwenden wir cURL in der Konsole, um die Datei herunterzuladen:

curl http://localhost:8080/2583_schoenenwerd.xtf | xmllint --format -

Mit xmllint wird das heruntergeladene XML noch schön formatiert. Der Formatumbau dauert für die Gemeinde Schönenwerd circa fünf Sekunden und das Resultat kann sich sehen lassen:

XTF Output

Ein Vorteil von ili2pg ist, dass es neben INTERLIS/XTF auch INTERLIS/GML exportieren kann. Der Anwender muss nur drei Buchstaben ändern (.xtf → .gml):

curl http://localhost:8080/2583_schoenenwerd.gml | xmllint --format -

Wiederum fünf Sekunden später erfreuen wir uns über die GML-Datei:

GML Output

Je nach Situation (Komplexität und Methode) könnte man auch den Datenumbau (1) gerade beim Aufruf erledigen. In unserem Fall wird der Datenumbau mit SQL-Befehlen gemacht und dauert weniger als eine Sekunde.

Eleganter wäre sicher auch wenn man auf das Zwischenspeichern in einer temporären Datei verzichten könnte und ili2pg direkt zum Klienten streamen könnte. Dann muss man auch nicht mehr den Knorz in den Zeilen 68 - 81 durchführen. Soweit ich das verstehe, dürfte das möglich sein, da die darunterliegende Klasse XtfWriter dies bereits unterstützt.

Und sollte aus ili2pg irgendwann einmal ein ili2GeoPackage werden, können auch Nicht-PostgreSQL-Anwender diese Bibliotheken verwenden. Man muss bloss den Datenumbau aus dem proprietären System nach GeoPackage machen und den Rest übernimmt ili2GeoPackage…​

Posted by Stefan Ziegler. | INTERLIS , ili2pg , Java