Přeskočit obsah

Mapové servery (GeoServer, MapServer) & Python klient (OWSLib)

V reálném (produkčním) nasazení je potřeba řešit zprovoznění nejen aplikační vrstvy mapového serveru, ale i webového serveru a jeho komunikace s mapovým serverem. V našem případě si tuto část výrazně zjednodušíme použitím předpřipravených Docker obrazů, které mají mapový a webový server již integrován v podobě izolovaného prostředí.

Docker je technologie sloužící pro izolaci aplikací ve formě kontejnerů (wikipedia).

Seznam příkazů:

  • docker pull
  • docker run
  • docker logs
  • docker exec
  • docker stop
  • docker rm

Mapové servery

GeoServer

GeoServer jestiť software otevřený pod licencí GNU GLPv2 určený pro sdílení, zpracování a editaci geoprostorových dat skrze internetové rozhraní. Často slouží jako spojovací článek mezi uživateli geoprostorových dat a daty samotnými. Jeho jádro je napsáno v programovacím jazyku Java. GeoServer započal svůj život roku 2001 jako projekt The Open Planning Project.

Mezi jeho nesporné výhody patří následující:

  • Data uživatelům poskytována ve formátech definovaných OGC
    • WMS
    • WFS
    • WCS
    • WPS
    • REST
  • Podpora mnoha formátů geoprostorových dat
  • Rozšířitelnost: Velké množství zásuvných modulů
  • Škálovatelný
  • Uživatelsky přístupné, klikací rozhraní

Nevýhody:

  • Uživatelsky přístupné, klikací rozhraní
    • Automatizace bývá zpravidla značně nepříjemná
  • Způsob, jakým GeoServer jako nováček rozeběhnout s lokálními daty, bývá trpký

GeoServer je dokonce implementovaný i jako plugin pro QGIS (bohužel, k roku 2024 proběhla poslední aktualizace roku 2016): https://plugins.qgis.org/plugins/geoserverexplorer/#plugin-about

Spuštění GeoServer

Budeme pracovati s oficiální docker obrazem docker.osgeo.org/geoserver. Ten si nejdřív stáhněme (docker pull) a následně jej spusťme (docker run) na pozadí (-d, není nutné). Přepínač -p slouží k přesměrování portu, na němž bude GeoServer přístupný. Přes parametr -v si připojíme adresář do /opt/geoserver_data/data - tento adresář nám umožní přidávati nová data do běžící instance GeoServeru.

docker pull docker.osgeo.org/geoserver:2.25.x
docker run -d -v /tmp/data_dir:/opt/geoserver_data/data -p 8080:8080 docker.osgeo.org/geoserver:2.25.x

Tip

Doporučujeme pro větší přehlednost svůj docker kontejner pojmenovat použitím --name my_geoserver

Přesvědčme se, zda naše instance běží v pořádku na adrese http://localhost:8080/geoserver.

Ve výchozím nastavení jsou administrátorské přihlašovací údaje následující:

username=admin
password=geoserver

Struktura dat

Jak si lze povšimnouti po přihlášení do rozhraní GeoServeru, data jsou strukturována následujícím způsobem:

  • workspaces
    • konvolut sloužící k organizaci sobě podobných vrstev dohromady
    • definován jménem (name) a identifikátorem (Namespace URI)
  • data stores
    • slouží k připojení datového zdroje obsahujícího rastrová či vektorová data
    • může to býti soubor či skupina souborů, tabulka v databázi, adresář
    • umožňuje definici parametrů spojení na jednom místě pro různé datové zdroje
  • layers
    • vrstvami jsou jednotlivé rastrové či vektorové datasety

Tvorba/upload dat

Vytvořme databázi zemětřesení. Vstupní data jsou otevřeně poskytována na stránkách USGS. Protože GeoServer bohužel nepodporuje data stores ve formátu GeoJSON, musíme je převésti do formátu jiného. K tomu můžeme využíti znalostí získaných v přednášce o knihovně a nástrojích GDAL/OGR.

ogr2ogr -of GPKG significant_month.gpkg significant_month.geojson

Nejprve vytvořme odpovídající workspace ve Workspaces -> Add new workspace.

Následně vytvořme odpovídající data store v Stores -> Add new store. V našem případě vyberme data store založený na formátu GeoPackage.

Jest vhodné nalezenou vrstvu následně publikovati.

Při publikaci dat definujme souřadnicový systém a nejmenší ohraničující obdélník. Nejsnáze tak učiníme skrze možnost Compute from native bounds.

Data si můžeme prohlédnout v záložce Layer Preview.

Styly

Protože výchozí styl pro bodová data není dostatečně sexy, pokusme se bodům přiřaditi vábnější design. Učiňme tak v záložce Styles -> Add a new style. Velkou výhodou je možnost generování stylů v software QGIS (jedná se o OGC standard SLD - Styled Layer Descriptor). Otevřeme si tedy data v QGISu a exportujme vhodný styl.

Důležité

Nezapomeňte při ukládání stylu zaškrtnout As SLD Style File

Následně si daný styl přiřaďme k vybrané vrstvě. Otevřeme-li si danou vrstvu, učiníme tak v záložce Publishing.

Následně data již uvidíme v námi definovaném stylu.

Webové služby

Náš GeoServer nyní poskytuje data jako WFS službu. Námi publikovaným workspaces odpovídají adresy ve formátu http://localhost:8080/geoserver/workspace/ows?service=WFS. WFS si můžeme připojiti na příklad jakožto novou vrstvu v prostředí QGIS (Layer -> Add Layer -> Add WFS Layer).

Tip

Preferujete-li GRASS GIS, můžete data importovati následujícím příkazem: v.in.wfs url="http://localhost:8080/geoserver/zemetreseni/ows?" out=zemetreseni_duben name=duben

Úkol

Učiňte totéž s exportem ve formě WMS.

MapServer

MapServer (https://mapserver.org/) je open source mapový server jehož vývoj započal již v polovině 90. let na univerzitě v Minnesotě (dříve byl označován jako UMN MapServer). Je publikován pod licencí MIT. Je napsán v jazyku C. Vyniká nejen svojí rychlostí, ale i malou paměťovou náročností (binární soubor má pouze několik desítek kilobajtů!).

Nastavení tohoto mapového serveru se typicky řídí pomocí jednoduchých konfiguračních souborů (tzv. "map files").

V následující části budeme použít sestavený Docker obraz dostupný z https://hub.docker.com/r/camptocamp/mapserver/. Obraz nejprve stáhneme k sobě na disk:

sudo docker pull camptocamp/mapserver:8.0

Vytvoříme pracovní adresář a přepneme se do něj:

mkdir -p mapserver && cd mapserver

WMS

V tomto adresáři vytvoříme konfigurační soubor mapové serveru (viz dokumentace).

Do konfigurace přidáme vrstvu (sekce LAYER ) vodních toků (cesta na GIS.labu: /mnt/repository/155FGIS/05/VodniTok.shp). Lokální adresář s daty (/mnt/repository/155FGIS) připojíme posléze do běžícího Docker kontejneru jako /mapfile. Proto v konfiguračním souboru uvedeme cestu /mapdata/05/data50/VodniTok.shp.

MAP
    NAME       "FGIS"
    STATUS     ON
    SIZE       600 400
    EXTENT     -904582 -1227334 -431727 -935354
    UNITS      meters
    IMAGECOLOR 255 255 255
    IMAGETYPE  PNG

    PROJECTION
       "init=epsg:5514"
    END

    WEB
        METADATA
            "wms_title"           "WMS FGIS Server"
            "wms_onlineresource"  "http://localhost:8090/?map=/etc/mapserver/fgis.map"
            "wms_srs"             "EPSG:5514"
            "wms_enable_request"   "*"
            "wms_format" "image/png"
        END
    END

    LAYER
        NAME "vodni_toky"
        STATUS ON
        TYPE LINE
        DATA "/mapdata/05/data50/VodniTok.shp"
        CLASS
            NAME "Vodní toky"
            STYLE
                COLOR 0 0 255
            END
        END
        METADATA
            "wms_title" "Vodní toky"
        END
    END
END

Tip

Prostorový rozsah datové kompozice můžeme zjistit pomocí příkazu knihovny GDAL ogrinfo:

ogrinfo /mnt/repository/155FGIS/05/VodniTok.shp -so -al | grep Extent

Následně spustíme instanci mapové serveru například na portu 8090:

sudo docker run -d \
  --rm --name mapserver \
  -v /mnt/repository/155FGIS:/mapdata/:ro \
  -v $(pwd)/fgis.map/:/etc/mapserver/fgis.map:ro \
  -p 8090:80 \
  camptocamp/mapserver:8.0

Běžící mapový server otestujeme. Nejprve vyzkoušíme dotaz typu GetCapabilities:

http://localhost:8090/?map=/etc/mapserver/fgis.map&service=WMS&request=GetCapabilities

Dále otestujeme dotaz GetMap, který vrací reálná data:

http://localhost:8090/?map=/etc/mapserver/fgis.map&service=WMS&version=1.3.0&request=GetMap&bbox=-904582,-1227334,-431727,-935354&layers=vodni_toky&styles=&crs=EPSG:5514&width=800&height=600&format=image/png

Úkol

Připojte WMS službu v prostředí QGISu a otestujte.

Do seznamu vrstev přidáme rastrová data:

    LAYER
        NAME "dtm"
        STATUS ON
        TYPE RASTER
        DATA "/mapdata/05/dtm1m.tif"
        METADATA
            "wms_title" "DTM"
        END
    END

Důležité

Po každé změně mapového souboru je nutné Docker kontejner restartovat:

sudo docker restart mapserver

Úkol

Vrstva DTM se zobrazuje pomalu z důvodu velkého prostorového rozlišení (1m) a navíc v nevhodné tabulce barev.

Tento nedostatek vyřešíme buď přidáním pyramid (GDAL příkaz gdaladdo) anebo převzorkováním do nižšího rozlišení (např. 10m) a nastavením vhodné tabulky barev. GDAL umožňuje nastavit tabulku barev ve formátu GeoTIFF pouze pro celočíselná data. Z tohoto důvodu převedeme hodnoty DTM na celočíselné. Tuto úpravu provedeme např. pomocí GRASS GIS:

grass -c EPSG:5514 wms_dtm
r.external -o in=/mnt/repository/155FGIS/05/dtm1m.tif out=dtm1m
r.colors map=dtm1m color=elevation
g.region -ap rast=dtm1m res=10
mkdir data
r.out.gdal -f in=dtm1m out=data/dtm10m.tif type=UInt16
exit
rm -r wms_dtm/

Upravte konfigurační soubor:

        DATA "/mapdata_local/dtm10m.tif"

Jelikož se jedná o 16bit data, je nutné přeškálování do rozsahu 0-255:

    PROCESSING "SCALE=auto"

Následně na to připojte do kontejneru lokální adresář s daty:

sudo docker stop mapserver
sudo docker run -d \
    --rm --name mapserver \
    -v /mnt/repository/155FGIS:/mapdata/:ro \
    -v $(pwd)/data:/mapdata_local/:ro \
    -v $(pwd)/fgis.map/:/etc/mapserver/fgis.map:ro \
    -p 8090:80 \
    camptocamp/mapserver:8.0

Dále si ukážeme přidání databázových datových zdrojů. Začneme formátem OGC GeoPackage, který je založen na souborové databázi SQLite:

    LAYER
        NAME "okresy"
        STATUS ON
        TYPE POLYGON
        CONNECTIONTYPE OGR
        CONNECTION "/mapdata/07/ruian_vusc.gpkg"
        DATA "okresy"
        CLASS
            NAME "Okresy"
            STYLE
                OUTLINECOLOR 0 0 0
                COLOR 150 150 150
            END
        END
        METADATA
            "wms_title" "Okresy"
        END
    END

Úkol

Zkuste přidat další vrstvu - městské části v Praze:

    LAYER
       NAME "praha_mc"
       STATUS ON
       TYPE POLYGON
       DATA "/mapdata/05/MAP_MESTSKECASTI_P.shp"
       CLASS
           NAME "Praha - městské části"
           STYLE
               OUTLINECOLOR 0 0 0
               COLOR 100 100 100
           END
       END
       METADATA
           "wms_title" "Praha - městské části"
       END
   END

Vyzkoušejte dotaz GetMap.

Pokud dotaz skončí chybou, tak se nejprve podívejme se na logovací zprávy:

sudo docker logs -f mapserver

Případně se do běžícího kontejneru přepneme a zkusíme otevřít datový zdroj pomocí GDAL příkazu ogrinfo:

sudo docker exec -it mapserver bash
ogrinfo /mapdata/05/MAP_MESTSKECASTI_P.shp
exit

V našem případě pomůže zpracovat soubor ve formátu Esri Shapefile pomocí knihovny GDAL:

ogr2ogr -f "Esri Shapefile" -lco ENCODING=UTF-8 \
    data/MAP_MESTSKECASTI_P.shp \
    /mnt/repository/155FGIS/05/MAP_MESTSKECASTI_P.shp

a aktualizovat konfigurační soubor:

       DATA "/mapdata_local/MAP_MESTSKECASTI_P.shp"

Nicméně důvod, proč MapServer nedokáže načíst data ve formátu Esri Shapefile dostupná z portálu otevřených dat IPR zůstává záhadou....

Úkol

Zkuste přidat podporu pro další souřadnicové systémy a otestujte (GetCapabilities):

        "wms_srs"             "EPSG:5514 EPSG:4326"

Výsledek ověřte v QGIS Data Source Manager:

Styly

Přidáme novou vrstvu vodních ploch, které kategorizujeme na základě atributu "DATA50_K". Styl definujeme v sekci CLASS.

Tip

Unikátní hodnoty lze zjistit i pomocí konzolového nástroje knihovny GDAL ogrinfo:

ogrinfo -sql "select distinct DATA50_K from VodniPlocha" /mnt/repository/155FGIS/05/data50/VodniPlocha.shp

Popis kategorie je uložen v atributu "DATA50_P".

    LAYER
        NAME "vodni_plochy"
        STATUS ON
        TYPE POLYGON
        DATA "/mapdata/05/data50/VodniPlocha.shp"
        LABELITEM "JMENO"
        ENCODING UTF-8
        CLASS
            NAME "Vodní plocha"
            EXPRESSION ([DATA50_K] = 3330000)
            STYLE
                OUTLINECOLOR 0 0 255
                COLOR 0 0 255
            END
            LABEL
                COLOR 20 20 20
                TYPE TRUETYPE
                SIZE 10
                POSITION LC
            END
        END
        CLASS
            NAME "Vodní tok nad 20 m šířky"
            EXPRESSION ([DATA50_K] = 3020500)
            STYLE
                OUTLINECOLOR 80 150 220
                COLOR 80 150 220
            END
        END
        METADATA
            "wms_title" "Vodní plochy"
        END
    END

Příklad výstupu:

WFS

WFS službu zprovozníme na základě dokumentace.

Sekci WEB > METADATA rozšíříme o

            "wfs_title"           "WFS FGIS Server"
            "wfs_onlineresource"  "http://localhost:8090/?map=/etc/mapserver/fgis.map"
            "wfs_srs"             "EPSG:5514 EPSG:4326"
            "wfs_enable_request"   "*"

Dále pro každou vrstvu (LAYER > METADATA) definujeme (příklad pro vrstvu vodních toků):

            "wfs_title" "Vodní toky"

Důležité

Pokud chcete publikovat u dané vrstvy i atributy prvků je nutné doplnit do sekce LAYER > METADATA:

         "gml_include_items" "all"

Běžící WFS server otestujeme. Nejprve vyzkoušíme dotaz typu GetCapabilities:

http://localhost:8090/?map=/etc/mapserver/fgis.map&service=WFS&request=GetCapabilities

Dále otestujeme dotaz GetFeatrure, který vrací vektorová data ve formátu OGC GML:

http://localhost:8090/?map=/etc/mapserver/fgis.map&service=WFS&version=2.0.0&request=GetFeature&bbox=-904582,-1227334,-431727,-935354&typename=okresy&crs=EPSG:5514

Úkol

Připojte WFS službu v prostředí QGISu a otestujte.

Python klient (OWSLib)

V následujících ukázkách se budeme dotazovat na námi provozované instance mapových serverů z pozice klienta.

Úkol

Ukázky níže pracují s instancí MapServer na portu 8090. Otestujte podobně i běžící instanci GeoServeru na portu 8080.

WMS

Připojíme se k WMS službě, stáhneme vybranou mapovou kompozici a uložíme na disk do souboru ve formátu PNG:

from owslib.wms import WebMapService

url = "http://localhost:8090/?map=/etc/mapserver/fgis.map"
wms = WebMapService(url)

# metadata
print(wms.identification.title)

# layer list
for name, con in wms.contents.items():
    print('-', name, con.boundingBox)

# download map composition (PNG)
img = wms.getmap(
    layers=["okresy", "praha_mc"],
    size=[800, 600],
    srs="EPSG:5514",
    bbox=[-904585.0, -1227300.0, -431724.0, -935237.0],
    format="image/png"
)
with open('wms_download.png', 'wb') as out:
    out.write(img.read())    

Příklad stažené kompozice vrstev:

WFS

Připojíme se k WFS službě, stáhneme vybrané vrstvy a uložíme na disk do souboru ve formátu OGC GML:

from owslib.wfs import WebFeatureService

url = "http://localhost:8090/?map=/etc/mapserver/fgis.map"
wfs = WebFeatureService(url, version='2.0.0')

# metadata 
print(wfs.identification.title)

# layer list
for name, con in wfs.contents.items():
    print('-', name, con.boundingBox)

# download data (GML)
features = wfs.getfeature(["ms:praha_mc"])
with open('wfs_download.gml', 'wb') as out:
    out.write(features.read())  

Poznámka

Pokud obsahuje GML chybovou hlášku "Web application error. Traditional BROWSE mode requires a TEMPLATE in the WEB section, but none was provided." je to pravděpodobně způsobeno chybou v knihovně OWSLib. Řešením by bylo změnit URL adresu mapového serveru tak, aby neobsahoval slovo "map".

ZABAGED

V této ukázce se zaměříme na mapový server poskytující reálná data, konkrétně ZABAGED® - Polohopis.

Pro území definované minimálním ohraničujícím obdélníkem (řádek 8) stáhneme všechny dostupné vrstvy ZABAGEDu. Pokud stažená vrstva neobsahuje žádné prvky (to zjistíme pomocí Python API knihovny GDAL), tak ji smažeme.

import os
from owslib.wfs import WebFeatureService
from osgeo import ogr

url = "https://ags.cuzk.cz/arcgis/services/ZABAGED_POLOHOPIS/MapServer/WFSServer"
wfs = WebFeatureService(url)

extent = (-775622.39, -1001081.93, -770049.81, -996485.77)

for layer in wfs.contents.keys():
    print(f"Processing {layer}...")
    # download
    features = wfs.getfeature(typename=[layer], bbox=extent,
                              srsname='urn:x-ogc:def:crs:EPSG:5514')
    output = f'{layer}.gml'
    with open(output, 'wb') as out:
        out.write(features.read())

    # delete layers with no features
    with ogr.Open(output) as ds:
        lyr = ds.GetLayer()
        delete = lyr is None or lyr.GetFeatureCount() < 1
    if delete:
        os.remove(output)
        # delete also GFS file created by GDAL
        os.remove(os.path.splitext(output)[0]+'.gfs')
        print(' - no features -> deleted')

Výsledek může vypadá následovně: