Aplikacja GIS – Spring, GDAL i MongoDB

GDAL

To już przerabialiśmy przy okazji konwersji ShapeFile do GeoJSON. Tym razem postanowiłem to wykorzystać do wczytania tracków z GPX. Jeśli wolisz parsować XML w inny sposób, to możesz spokojnie olać ten fragment.

Największy ból tyłka, to kompilacja tegoż pakietu wraz z wrapperem javy.
Na hostingu, którego używam, należy pobrać i rozpakować pakiet GDAL, a następnie użyć configure, wskazując ścieżki do JVM.

./configure --prefix=/home/globalbus/ --with-expat --without-libtool --with-java=/usr/local/openjdk8/ --with-jvm-lib-add-rpath=yes --with-jvm-lib=/usr/local/openjdk8/jre/lib/amd64/server/ --enable-shared --with-pic

Ważną rzeczą jest prefix. Ponieważ nie mam praw roota, to pakiet jest instalowany wewnątrz katalogu home. Nie stanowi to problemu, o ile wiemy co robimy.

Wydajemy polecenie make (gmake na BSD) i liczymy na to, że kompilacja się nie wywali. Jeśli wszystko się udało, to dajemy make install i przechodzimy do katalogu swig/java i tam znowu make, make install. To zainstaluje natywne biblioteki i wrappery JNI. Potrzebujemy jeszcze jar z kodem Javy. Budujemy go poleceniem ant (trzeba ściągnąć apache-ant). Niby jest ten kod dostępny w repo mavena, ale w praktyce działa tylko ta biblioteka, którą sami zbudujemy, na tej konkretnej maszynie. Inaczej całość się wywala. Wynikowy gdal.jar, dodajemy do classpath (gradle files(‚gdal.jar’)).

Aby użyć biblioteki, musimy dodać dwie zmienne systemowe, do katalogów z wrapperami i share.

systemProperty 'GDAL_DATA', "/home/globalbus/share/gdal/"
systemProperty 'java.library.path', '/home/globalbus/lib'

Ponadto, należy wyeksportować zmienną systemową LD_LIBRARY_PATH, aby linker najpierw brał biblioteki z naszego katalogu lib, a nie z systemowych wersji

LD_LIBRARY_PATH=/home/globalbus/lib:/usr/local/lib:/usr/local/lib/gcc48/

Uff. Sprawdzamy jeszcze, czy sam GDAL działa, np przez ogrinfo plik.gpx. Jeśli coś się pokaże i nie wysypie się błędem, to znaczy, że jest w miarę dobrze. Jeśli brakuje jakiejś biblioteki systemowej, to dodajemy ścieżkę do linkera, od końca.

Niby możemy już użyć kodu GDAL w aplikacji javy, ale jest jedno małe „ale”. Specyfika JNI. Ten mechanizm polega na załadowaniu biblioteki natywnej do pamięci przez System.loadLibrary(). Zwykle dzieje się to przy ładowaniu klasy-wrappera. Sęk w tym, że każdą bibliotekę można załadować tylko raz. Nie można zatem powołać do życia dwóch wrapperów lub też zresetować ich stanu. Pełny static.
Ok, ale co się dzieje przy deploy/undeploy na kontenerze servletów? Klasy są wywalane z pamięci i ładowane raz jeszcze. Teraz wrapper nic nie wie o tym, że biblioteka jest już załadowana, próbuje loadLibrary raz jeszcze i dostajemy Exception. Rozwiązaniem jest przygotowanie kodu korzystającego z klas wrappera jako shared library, które nie jest przeładowywanie przy deploymencie aplikacji. Wystarczy wrzucić jar do lib w kontenerze, a w dependency oznaczyć go jako „provided” (nie trafi do wynikowego archiwum war).

Schemat konwersji wygląda tak

public class GdalFacade {
    static {
        ogr.RegisterAll();
        ogr.UseExceptions();
    }

    public List<String> fileToGeoJson(String absolutePath, int geometryType) {
        DataSource dataSource=null;
        try {
            List<String> geojsons = new ArrayList<>();
            dataSource = ogr.Open(absolutePath);
            for (int i = 0; i < dataSource.GetLayerCount(); i++) {
                Layer layer = dataSource.GetLayer(i);
                if (layer.GetFeatureCount() == 0)
                    continue;
                while (true) {
                    Feature feature = layer.GetNextFeature();
                    if (feature == null)
                        break;
                    Geometry geometry = feature.GetGeometryRef();
                    if (feature.GetDefnRef().GetGeomType() == geometryType)
                        geojsons.add(geometry.ExportToJson());


                }
            }
            return geojsons;
        }
        finally{
            if(dataSource!=null)
                dataSource.delete();
        }
    }
}

W statycznym kodzie ładujemy wszystkie Drivery DataSource i każemy rzucać wyjątkami, gdy coś spieprzymy (co jest praktyczniejsze niż zwracane nulle). Dalej prosto, otwórz plik, przeszukaj warstwy, przeszukaj Feature (tutaj to elementy GPX), sprawdź czy GeometryType jest zgodne (dla tracków GPX geometryType to MultiLineString.) Jak jest, to eksportuj do JSON. W ogólności, w jednym GPX może być wiele tracków.

Ważne jest zamknięcie pliku. Ponieważ korzystamy z natywnego kodu, to tutaj Garbage Collector nie działa i trzeba pamiętać o delete().

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *