Skip to content

Commit

Permalink
New GRASS GIS addons to publish and style raster maps in Geoserver (#1)
Browse files Browse the repository at this point in the history
* add addons

* Apply suggestions from code review

Co-authored-by: Markus Neteler <neteler@osgeo.org>

* lint

* Apply suggestions from code review

Co-authored-by: Markus Neteler <neteler@osgeo.org>

* start fixing tests

Co-authored-by: Markus Neteler <neteler@osgeo.org>
  • Loading branch information
mmacata and neteler authored Dec 1, 2022
1 parent 0154209 commit 845b511
Show file tree
Hide file tree
Showing 15 changed files with 1,887 additions and 6 deletions.
8 changes: 4 additions & 4 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
exclude = .git
max-line-length = 88
per-file-ignores =
./r.geoserver.publish.py: E501, F821
./r.geoserver.style.py: E501, F821
./testsuite/test_r_geoserver_style.py: E501
./t.geoserver.publish.py: E501, F821
./r.geoserver.publish/r.geoserver.publish.py: E501, F821
./r.geoserver.style/r.geoserver.style.py: E501, F821
./r.geoserver.style/testsuite/test_r_geoserver_style.py: E501
./t.geoserver.publish/t.geoserver.publish.py: E501, F821
2 changes: 1 addition & 1 deletion .github/workflows/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
set -e

# download NC test loaction if the test needs the data and run tests
if [ $1 == "NC" ]
if [ "$1" == "NC" ]
then
g.extension g.download.location
g.download.location url=https://grass.osgeo.org/sampledata/north_carolina/nc_spm_full_v2alpha2.tar.gz path=/grassdb
Expand Down
2 changes: 1 addition & 1 deletion r.geoserver.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ <h2>DESCRIPTION</h2>
<h2>REQUIREMENTS</h2>

<ul>
<li>A running <a href="https://geoserver.org/">GeoServer</a> instance</a></li>
<li>A running <a href="https://geoserver.org/">GeoServer</a> instance</li>
</ul>

<h2>AUTHORS</h2>
Expand Down
7 changes: 7 additions & 0 deletions r.geoserver.publish/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
MODULE_TOPDIR = ../..

PGM = r.geoserver.publish

include $(MODULE_TOPDIR)/include/Make/Script.make

default: script
60 changes: 60 additions & 0 deletions r.geoserver.publish/r.geoserver.publish.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<h2>DESCRIPTION</h2>

<em>r.geoserver.publish</em> is a GRASS GIS addon Python script to publish
raster maps as well as space time raster data sets with
<a href="https://mundialis.github.io/geoserver-grass-raster-datastore/">geoserver-grass-datastore</a> and creates a GeoServer layer for it.
The module uses the GeoServer REST API.
<p>
It is important that you are in the mapset in which the raster map or space time
raster data set is stored.
<p>
If you have a raster map and a space time raster map with the same name, which
you use as <b>input</b>, then you have to set the <b>type</b> to "raster" or
"strds".

<h2>EXAMPLES</h2>

<h3>Publish raster map to GeoServer</h3>

Publish elevation map to GeoServer and set the style:

<div class="code"><pre>
# publish layer
r.geoserver.publish input=elevation \
host=http://geoserver port=8080 user=admin password=geoserver \
workspace=spieltag coveragestore=elevation layername=elevation \
gs_file_path=/grassdb/nc_spm_08/PERMANENT
# set style (applies current map color table)
r.geoserver.style host=http://geoserver port=8080 user=admin password=geoserver \
workspace=spieltag layername=elevation
</pre></div>

<h3>Publish space time raster data set to geoserver</h3>

Publish MODIS LST space time raster data to geoserver and set style (a mapset
with the example mapset of the MODIS data is available here:

<a href="https://grass.osgeo.org/sampledata/north_carolina/nc_spm_mapset_modis2015_2016_lst.zip">nc_spm_mapset_modis2015_2016_lst.zip</a>):
<div class="code"><pre>
# publish layer
r.geoserver.publish input=LST_Day_monthly \
host=http://geoserver port=8080 user=admin password=geoserver \
workspace=spieltag coveragestore=modis_lst_strds layername=LST_Day_monthly \
gs_file_path=/grassdb/nc_spm_08_grass7/modis_lst
# set style (applies color table of first map)
r.colors MOD11B3.A2015001.h11v05.single_LST_Day_6km color=bcyr
r.geoserver.style host=http://geoserver port=8080 user=admin password=geoserver \
workspace=spieltag layername=LST_Day_monthly
</pre></div>

<h2>SEE ALSO</h2>

<em>
<a href="r.out.gdal.html">r.out.gdal</a>,
<a href="r.out.geoserver.html">r.out.geoserver</a>,
<a href="r.geoserver.style.html">r.geoserver.style</a>
</em>

<h2>AUTHORS</h2>

Anika Weinmann and Carmen Tawalika, <a href="https://www.mundialis.de/">mundialis</a>
289 changes: 289 additions & 0 deletions r.geoserver.publish/r.geoserver.publish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
#
############################################################################
#
# MODULE: r.geoserver.publish
# AUTHOR(S): Anika Weinmann
# Carmen Tawalika
#
# PURPOSE: Publishes a raster map and space time raster data sets with
# geoserver-grass-datastore
# COPYRIGHT: (C) 2019-2022 by mundialis GmbH & Co. KG and the GRASS Development Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#############################################################################
# %Module
# % description: Publishes a raster map or STRDS set through the geoserver-grass-raster-datastore.
# % keyword: geoserver-grass-datastore
# % keyword: raster
# % keyword: temporal
# % keyword: geoserver
# %End

# %option
# % key: input
# % type: string
# % required: yes
# % multiple: no
# % label: Name of raster map or space time raster dataset
# %end

# %option
# % key: host
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer host including URI scheme
# %end

# %option
# % key: port
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer port
# %end

# %option
# % key: user
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer user
# %end

# %option
# % key: password
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer password
# %end

# %option
# % key: workspace
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer workspace
# %end

# %option
# % key: coveragestore
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer coveragestore
# %end

# %option
# % key: layername
# % type: string
# % required: yes
# % multiple: no
# % label: Layername
# %end

# %option
# % key: title
# % type: string
# % required: no
# % multiple: no
# % label: Layer title
# %end

# %option
# % key: gs_file_path
# % type: string
# % required: yes
# % multiple: no
# % label: GeoServer filepath to GRASS mapset
# %end

# %option
# % key: type
# % type: string
# % required: no
# % multiple: no
# % options: raster,strds,auto
# % label: Data set type of input
# % answer: auto
# %end

# TODO test if layer already exists

import json
import os
import requests
from grass.script import core as grass


def main():

input = options["input"].split("@")[0]
type = options["type"]
host = options["host"]
port = options["port"]
user = options["user"]
pw = options["password"]
workspace = options["workspace"]
coveragestore = options["coveragestore"]
layername = options["layername"]
if options["title"]:
title = options["title"]
else:
title = layername
gs_file_path = options["gs_file_path"]

# check location
location = gs_file_path.split(os.sep)[-2]
mapset = gs_file_path.split(os.sep)[-1]
with open(os.environ["GISRC"], "r") as f:
gisrc = f.read()
current_location = [
line.split(":")[1].strip()
for line in gisrc.splitlines()
if line.startswith("LOCATION_NAME")
][0]
if location != current_location:
grass.fatal(
_(
"The <gs_file_path> contains a location which is not "
"the current location"
)
)

# get projection of current location
proj = grass.parse_command("g.proj", flags="g")
if "epsg" in proj:
epsg = proj["epsg"]
else:
epsg = proj["srid"].split("EPSG:")[1]

# check type and build path
strds_exists = False
if type == "auto" or type == "strds":
strds_in_mapset = [
strds.split("@")[0] for strds in grass.parse_command("t.list")
]
if input in strds_in_mapset:
strds_exists = True
gs_file_path = os.path.join(gs_file_path, "tgis", "sqlite.db")
type = "strds"
raster_exists = False
if type == "auto" or type == "raster":
raster_in_mapset = [
rast.split("@")[0] for rast in grass.parse_command("g.list", type="raster")
]
if input in raster_in_mapset:
raster_exists = True
gs_file_path = os.path.join(gs_file_path, "cellhd", input)
type = "raster"
if raster_exists and strds_exists:
grass.fatal(_(f"There is both a strds and a raster with name <{input}>"))
elif raster_exists is False and strds_exists is False:
grass.fatal(_(f"Input <{input}> does not exist"))

headers = {"content-type": "application/json"}

grass.message(_("Create workspace if not exists..."))
workspace_dict = {"workspace": {"name": workspace}}

postbody = json.dumps(workspace_dict)
url = "%s:%s/geoserver/rest/workspaces" % (host, port)
resp = requests.post(url, headers=headers, data=postbody, auth=(user, pw))
if resp.status_code != 201:
if resp.status_code == 401:
grass.message(_("Workspace already exists"))
else:
grass.fatal(_("Creation of workspace failed!"))
else:
grass.message(_("Workspace did not exist and was created."))

grass.message(_("Create coveragestore..."))
coveragestore_dict = {
"coverageStore": {
"name": coveragestore,
"type": "GRASS",
"enabled": True,
"workspace": {"name": workspace},
"url": "file:%s" % gs_file_path,
}
}
postbody = json.dumps(coveragestore_dict)
url = "%s:%s/geoserver/rest/workspaces/%s/coveragestores" % (host, port, workspace)
resp = requests.post(url, headers=headers, data=postbody, auth=(user, pw))
if resp.status_code != 201:
grass.fatal(_("Creation of coveragestore failed!"))

grass.message(_("Create layer..."))
if type == "raster":
metadata = {}
else:
metadata = {
"entry": [
{"@key": "elevation", "dimensionInfo": {"enabled": False}},
{
"@key": "time",
"dimensionInfo": {
"enabled": True,
"presentation": "LIST",
"units": "ISO8601",
"defaultValue": {
"strategy": "NEAREST",
"referenceValue": "CURRENT",
},
"nearestMatchEnabled": False,
},
},
{"@key": "dirName", "$": f"{coveragestore}_{layername}"},
]
}

coverage_dict = {
"coverage": {
"description": "Generated from GRASS GIS",
"enabled": True,
"nativeCoverageName": f"{input}@{mapset}",
"keywords": {"string": ["WCS", title]},
"name": layername,
"namespace": {"name": workspace},
"metadata": metadata,
"srs": "EPSG:%s" % str(epsg),
"store": {
"@class": "coverageStore",
"name": "%s:%s" % (workspace, coveragestore),
},
"title": title,
}
}

postbody = json.dumps(coverage_dict)
url = "%s:%s/geoserver/rest/workspaces/%s/coveragestores/%s/coverages" % (
host,
port,
workspace,
coveragestore,
)
resp = requests.post(url, headers=headers, data=postbody, auth=(user, pw))
if resp.status_code != 201:
grass.fatal(
_("Creation of coverage failed! \n <%s> \n <%s>")
% (str(resp.status_code), resp.text)
)
else:
grass.message(_("Creation of coverage succeeded!"))


if __name__ == "__main__":
options, flags = grass.parser()
main()
Loading

0 comments on commit 845b511

Please sign in to comment.