11 February 2016

Seit kurzem gibt es neu auch ili2gpkg. Damit kann aus einer INTERLIS-Transferdatei schnell und ohne eine PostgreSQL-Datenbank am Laufen zu haben eine GeoPackage-Datei erstellt werden. Diese kann dann in QGIS oder ArcGIS visualisert und bearbeitet werden. Der Befehl zum Erstellen des GeoPackages ist praktisch identisch dem Befehl zum Importieren in eine PostGIS-Datenbank:

java -jar ili2gpkg.jar --import --nameByTopic --modeldir http://models.geo.admin.ch --models DM01AVCH24D --dbfile av_solothurn.gpkg ch_260100.itf

Im Prinzip stehen die gleichen Optionen wie bei ili2pg zur Verfügung. Einige gibt es natürlich nicht, z.B. die DB-Connection-Parameter. Die werden einfach durch den Filenamen-Parameter ersetzt: --dbfile. Natürlich ist man dabei nicht nur auf das Erstellen (= Lesen von INTERLIS) von GeoPackage-Dateien beschränkt. Man kann ebenso aus GeoPackage-Dateien INTERLIS-Transfer-Dateien erstellen. Dazu in einem späteren Beitrag mehr.

Eine Anwendungsmöglichkeit unter vielen ist z.B. ein kleiner Webdienst, wo man eine INTERLIS-Transferdatei hochladen kann und als Antwort die GeoPackage-Datei bekommt. Klassischerweise würde man hier ein Java-Servlet schreiben. Fürs Prototyping erstelle ich aber ein Groovlet. Man kann sich das auf dem Web/Servlet-Container so einrichten, dass alle in einem Ordner liegende *.groovy-Dateien als Servlet resp. eben als Groovlet ausgeführt werden. Zum Rumspielen noch ganz praktisch.

Als erstes brauchen wir das Upload-Formular. Dazu reichen ein paar Zeichen Groovy, das uns das benötigte HTML erstellt:

html.html {
    head {
        meta(charset:'utf-8')
        meta(name:'viewport', content:'width=device-width, initial-scale=1')
        meta('http-equiv':'x-ua-compatible', content:'ie=edge')

        title 'ili2gpkg online converter'
        style '''
            label { display: block; padding: 0.2em; }
        '''
    }
    body {
        h1 'INTERLIS (ITF/XTF) to GeoPackage Converter'
        form action: 'do_ili2gpkg.groovy', method: 'post', enctype: 'multipart/form-data', {
            label 'Reference frame: ', {
                select name: 'reference_frame', {
                    option 'LV03'
                    option 'LV95'
                }
            }
            label "--strokeArcs", {
                input type: 'checkbox', checked: 'checked', name: 'strokeArcs', id: 'strokeArcs'
            }
            label "--skipPolygonBuilding", {
                input type: 'checkbox', name: 'skipPolygonBuilding', id: 'skipPolygonBuilding'
            }
            label "--nameByTopic", {
                input type: 'checkbox', checked: 'checked', name: 'nameByTopic', id: 'nameByTopic'
            }
            label 'File: ', {
                input type: 'file', name: 'file'
            }
            input type: 'submit', name: 'submit', value: 'Send to server'
        }
    }
}

Das sind ganz schön nach 90er-Jahre aus, aber es fehlt schliesslich auch jegliches Styling:

ili2gpkg upload formular

Von den unzähligen Programmoptionen lassen wir nur vier zu, die der Anwender via Webformular auswählen kann:

  • --defaultSrsCode: Entweder LV03 oder LV95.

  • --skipPolygonBuilding: Falls gewünscht, wird die Flächenbildung für Polygone nicht gemacht und es bleiben «Spaghetti»-Daten. Dies kann sehr praktisch für die Verifikation von Datenlieferungen sein. Bei der Flächenbildung bereinigt ili2gpkg zulässige Overlaps, um OGC-konforme Geometrien zu erhalten. Um jedoch wirklich die Original-Geometrien zu erhalten und diese bei Grenzfällen besser beurteilen zu können, lohnt es sich manchmal nur die Linien zu betrachten.

  • --strokeArcs: Kreisbogen werden segmentiert.

  • --nameByTopic: Die Tabellen in der GeoPackage-Datenbank werden nach dem Muster Topicname_Classname benannt.

Wie man dem Groovy-Skript resp. der HTML-Datei entnehmen kann, ist do_ili2gpkg.groovy das Groovy-Skript, das beim Senden aufgerufen wird. Dieses übernimmt die eigentliche Umwandlung INTERLIS → GeoPackage:

@Grapes([
   @GrabResolver(name='catais.org', root='http://www.catais.org/maven/repository/release/', m2Compatible='true'),
   @Grab(group='commons-fileupload', module='commons-fileupload', version='1.3.1'),
   @Grab(group='commons-io', module='commons-io', version='2.4'),
   @Grab(group='org.xerial', module='sqlite-jdbc', version='3.8.11.2'),
   @Grab('ch.interlis:ili2c:4.5.21'),
   @Grab('ch.interlis:ili2gpkg:3.0.0'),
   //@GrabConfig(systemClassLoader = true)
])

import javax.servlet.http.HttpServletResponse
import javax.servlet.ServletOutputStream
import java.util.logging.Logger
import java.nio.file.Path
import java.nio.file.Files
import org.apache.commons.io.IOUtils
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.fileupload.FileItem
import org.apache.commons.fileupload.util.Streams
import org.apache.commons.fileupload.servlet.ServletFileUpload
import org.apache.commons.fileupload.disk.DiskFileItemFactory
import org.apache.commons.fileupload.FileUploadException
import ch.ehi.ili2db.base.Ili2db
import ch.ehi.ili2db.base.Ili2dbException
import ch.ehi.ili2db.gui.Config
import ch.ehi.ili2db.mapping.NameMapping
import ch.ehi.basics.logging.EhiLogger

Logger logger = Logger.getLogger("do_ili2gpkg.groovy")
logger.setUseParentHandlers(true)
logger.info ("Starts at: " + new Date())

if (ServletFileUpload.isMultipartContent(request)) {
    ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory())
    //upload.setSizeMax(52428800) // 50MB
    upload.setSizeMax(2*5242880) // 2*5MB

    List<FileItem> items = null

    try {
        items = upload.parseRequest(request);
    } catch (FileUploadException e) {
        logger.severe e.getMessage()
        response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage())
        return
    }

    for (item in items) {
        if (item.isFormField()) { // 'normal' form fields
            String fieldName = item.getFieldName()
            String value = item.getString()

            params[fieldName] = value
            logger.info fieldName.toString()
            logger.info value.toString()
            logger.info item.getClass().toString()

        } else { // files
            String fieldName = item.getFieldName()
            String fileName = FilenameUtils.getName(item.getName())

            if (fileName.equalsIgnoreCase("")) {
                // return 'bad request' (400) if no file was sent
                String errorMessage = "No file chosen."
                logger.severe errorMessage
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, errorMessage)
                return
            }

            // get the file as input stream
            InputStream fileContent = item.getInputStream()

            // create random temporary directory
            String tmpDirPrefix = "ili2gpkg_";
            Path tmpDir = Files.createTempDirectory(tmpDirPrefix);

            // copy input stream into target file
            String targetFileName = fileName
            File targetFile = new File(tmpDir.toString() + File.separator + targetFileName)

            try {
                FileUtils.copyInputStreamToFile(fileContent, targetFile)
                logger.info "Uploaded file: " + targetFile.toString()
                logger.info "Uploaded file size [KB]: " + (int) (targetFile.length() / 1024)
            } catch (java.io.IOException e) {
                FileUtils.deleteDirectory(tmpDir.toFile())

                logger.severe e.getMessage()
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage())
                return
            }

            // create configuration for ili2gpkg
            def config = initConfig(params)

            // Set the name of the geopackage.
            // Same name as input file but with *.gpkg extension instead.
            String gpkgFileName = FilenameUtils.removeExtension(targetFileName) + ".gpkg"
            gpkgFullFileName = tmpDir.toString() + File.separator + gpkgFileName
            config.setDbfile(gpkgFullFileName)
            config.setDburl("jdbc:sqlite:"+config.getDbfile())
            config.setXtffile(targetFile.toString())

            String fileExtension = FilenameUtils.getExtension(targetFileName)
            System.out.println(fileExtension)
            if (fileExtension.equalsIgnoreCase("itf")) {
                config.setItfTranferfile(true)
            }

            // Now create the GeoPackage.
            try {
                //EhiLogger.getInstance().setTraceFilter(false)
                Ili2db.runImport(config, "")
            } catch (Ili2dbException e) {
                logger.severe e.getMessage()
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage())
                return
            }

            // Send GeoPackage to browser.
            response.setContentType("application/x-sqlite3")
            response.setHeader("Content-Disposition", "attachment; filename=" + gpkgFileName);
            ServletOutputStream os = response.getOutputStream();
            FileInputStream fis = new FileInputStream(gpkgFullFileName);
            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()
                }
            } catch (Exception e) {
                logger.severe e.getMessage()
            }
            finally {
                FileUtils.deleteDirectory(tmpDir.toFile())
            }
        }
    }
}

def initConfig(params) {
    def config = new Config()
    config.setModeldir("http://models.geo.admin.ch/;http://models.geo.gl.ch/;http://www.catais.org/models")
    config.setModels(Ili2db.XTF)
    config.setSqlNull("enable");
    config.setDefaultSrsAuthority("EPSG");
    config.setDefaultSrsCode("21781");
    config.setMaxSqlNameLength(Integer.toString(NameMapping.DEFAULT_NAME_LENGTH));
    config.setIdGenerator(ch.ehi.ili2db.base.TableBasedIdGen.class.getName());
    config.setInheritanceTrafo(config.INHERITANCE_TRAFO_SMART1);
    config.setCatalogueRefTrafo(Config.CATALOGUE_REF_TRAFO_COALESCE);
    config.setMultiSurfaceTrafo(Config.MULTISURFACE_TRAFO_COALESCE);
    config.setMultilingualTrafo(Config.MULTILINGUAL_TRAFO_EXPAND);

    config.setGeometryConverter(ch.ehi.ili2gpkg.GpkgColumnConverter.class.getName());
    config.setDdlGenerator(ch.ehi.sqlgen.generator_impl.jdbc.GeneratorGeoPackage.class.getName());
    config.setJdbcDriver("org.sqlite.JDBC");
    config.setIdGenerator(ch.ehi.ili2db.base.TableBasedIdGen.class.getName());
    config.setIli2dbCustomStrategy(ch.ehi.ili2gpkg.GpkgMapping.class.getName());
    config.setOneGeomPerTable(true);

    for (param in params) {
        def key = param.key

        if (key.equalsIgnoreCase("strokeArcs")) {
            config.setStrokeArcs(config.STROKE_ARCS_ENABLE)
        }

        if (key.equalsIgnoreCase("nameByTopic")) {
            config.setNameOptimization(config.NAME_OPTIMIZATION_TOPIC)
        }

        if (key.equalsIgnoreCase("skipPolygonBuilding")) {
            config.setDoItfLineTables(true);
            config.setAreaRef(config.AREA_REF_KEEP);
        }

        if (key.equalsIgnoreCase("reference_frame")) {
            if (param.value.equalsIgnoreCase("LV95")) {
                config.setDefaultSrsCode("2056");
            }
        }
    }
    return config
}

logger.info ("Stops at: " + new Date())

Zeilen 1 - 9: Die notwendigen Bibliotheken werden mittels Grape einmalig heruntergeladen. Die *.jar-Dateien landen dann im .groovy/grapes/-Verzeichnis des Apache-Tomcat-Users und nicht etwa im lib-Verzeichnis von Apache-Tomcat selbst. Der Befehl @GrabConfig(systemClassLoader = true) scheint nicht notwendig zu sein, wenn das Skript als Groovlet in Apache Tomcat läuft.

Zeilen 11 - 28: Notwendige Imports werden gemacht. Für den File-Upload verwenden wir die Apache-Commons-Bibliotheken. Ab Servlet-Spezifikation 3.0 gibt es dafür native Unterstützung. Heruntergebrochen auf ein Groovlet habe ich das aber nicht zum Laufen gebracht. Daher wird hier wieder die old-school-Methode verwendet.

Zeile 37: Die maximale Grösse der Upload-Datei ist auf 10MB beschränkt.

Zeilen 49 - 58: «Normale» Parameter aus dem HTML-Formular werden in einer Map gespeichert. Sie werden später für die Konfiguration von ili2gpkg verwendet.

Zeilen 59 - 92: Die gelieferte Datei wird entgegengenommen und in einem temporären Verzeichnis gespeichert.

Zeilen 99 - 119: Hier findet die eigentliche Umwandlung der INTERLIS-Transferdatei in die GeoPackage-Datei statt. Die Config-Klasse wird mit den übermittelten Parameter aus dem HTML-Formular in einer separaten Methode konfiguriert. Ein INTERLIS-Modell oder ein -Repository kann nicht übermittelt werden. Das Modell wird aus der Transferdatei selber ermittelt (Zeile 148) und in den drei hardcodierten Repositories gesucht.

Zeilen 121 - 140: Zu guter Letzt wird die GeoPackage-Datei an den Browser zurückgesendet. Der Benutzer muss sie nur noch speichern.

ili2gpkg download

Das Resultat sieht genauso aus wie wir es von ili2pg gewohnt sind. Nur halt in einer GeoPackage-Datei:

ili2gpkg sqliteman

Anschauen kann man sich das Resultat anschliessend in QGIS:

ili2gpkg qgis

Achtung: ili2gpkg setzt den Layer-Extent gemäss dem INTERLIS-Modell. Beim CH-Modell der amtlichen Vermessung entspricht das der Bounding-Box der gesamten Schweiz. Wenn man in QGIS «Zoom to Layer Extent» wählt, zoomt QGIS auf die gesamte Schweiz. Einerseits verwirrend, andererseits eigentlich korrekt. Aber darüber kann man sich sicher lange unterhalten.

In Apache Tomcat sind drei Anpassungen vorzunehmen. Einerseits muss konfiguriert werden, dass alle *.groovy-Dateien eines Verzeichnisses als Groovlet ausgeführt werden. Dies wird in der web.xml-Datei gemacht (im Root-Element):

<servlet>
    <servlet-name>Groovy</servlet-name>
    <servlet-class>groovy.servlet.GroovyServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Groovy</servlet-name>
    <url-pattern>*.groovy</url-pattern>
</servlet-mapping>

Zudem wollen wir dieses Verzeichnis an einem beliebigen Ort im Filesystem haben und nicht unterhalb des Tomcat-Installationsverzeichnisses. Dazu setzen wir einen «Context Path» in der Datei server.xml unter <Host>:

<Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true">

  <!-- SingleSignOn valve, share authentication between web applications
       Documentation at: /docs/config/valve.html -->
  <!--
  <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
  -->

  <!-- Access log processes all example.
       Documentation at: /docs/config/valve.html
       Note: The pattern used is equivalent to using pattern="common" -->
  <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
         prefix="localhost_access_log" suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />

 <!-- Groovlets -->
 <Context path="/groovy/ili2gpkg" docBase="/home/stefan/Projekte/ili2gpkg_service/scripts" reloadable="true"/>

</Host>

Einzig die Zeilen 17 und 18 stammen von mir, alles Andere sind Default-Einstellungen. Der Eintrag bewirkt jetzt, dass der Request http://…​/groovy/ili2gpkg/mein_skript.groovy (Attribut path) im Verzeichnis gemäss dem Attribut docBase das Groovy-Skript mein_skript.groovy sucht und, sofern vorhanden, ausführt.

Als letztes muss noch die groovy-all-x.y.z.jar in das lib-Verzeichnis von Apache Tomcat kopiert werden. Dieses Groovy-«Sorglos»-Paket liegt dem Download bei.

Ein Live-Beispiel gibt es hier(mspublic / mspublic). Ein erweitertes Beispiel findet sich hier.

Posted by Stefan Ziegler. | INTERLIS , ili2gpkg , ili2pg , Java , Tomcat , Servlet