save model properties to a file (GRASS Model File|*.gxm)
export model to Python script
export model to Python script in the form of a PyWPS process
+
export model to an actinia process
export model to image file
@@ -57,7 +58,7 @@
Main dialog
(2) Load model from file,
(3) Save current model to file,
(4) Export model to image,
-(5) Export model to Python script,
+(5) Export model to a (Python/PyWPS/actinia) script,
(6) Add command (GRASS module) to model,
(7) Add data to model,
(8) Manually define relation between
@@ -82,9 +83,9 @@
Main dialog
There is also a lower menu bar in the Graphical modeler dialog where one can
manage model items, visualize commands, add or manage model variables,
-define default values and descriptions. The Python editor dialog window
-allows seeing workflows written in Python code, either as a basic Python
-script, or as a PyWPS script. The rightmost tab of the bottom menu is
+define default values and descriptions. The Script editor dialog window
+allows seeing and exporting workflows as basic Python scripts, as PyWPS
+scripts, or as actinia processes. The rightmost tab of the bottom menu is
automatically triggered when the model is activated and shows all the steps
of running GRASS modeler modules; in the case some errors occur in
the calculation process, they are are written at that place.
@@ -220,7 +221,7 @@
Managing model parameters
-The final model, the list of all model items, and the Python code window with
+The final model, the list of all model items, and the Script editor window with
Save and Run option are shown in the figures below.
@@ -237,7 +238,7 @@
Managing model parameters
-Figure: Items with Python editor window.
+Figure: Items with Script editor window.
@@ -345,8 +346,8 @@
Handling intermediate data
Figure: Usage and definition of intermediate data in model.
-
Using the Python editor
-By using the Python editor in the Graphical Modeler the user can add Python code and then
+
Using the Script editor
+By using the Script editor in the Graphical Modeler, the user can add Python code and then
run it with Run button or just save it as a Python script *.py.
The result is shown in the Figure below:
@@ -356,11 +357,11 @@
Using the Python editor
-Figure: Python editor in the wxGUI Graphical Modeler.
+Figure: Script editor in the wxGUI Graphical Modeler.
-In the Script type combobox, the user can also choose a PyWPS export
-instead of a basic Python script. A PyWPS process based on the model will be
+The second option in the Script type combobox exports a PyWPS script
+instead of a basic Python one. A PyWPS process based on the model will be
generated then; for the PyWPS script, the Run button is disabled as
users are expected to include this script in their web processing service and
not to run it on itself.
@@ -369,7 +370,20 @@
Using the Python editor
-Figure: Python editor in the wxGUI Graphical Modeler - set to PyWPS.
+Figure: Script editor in the wxGUI Graphical Modeler - set to PyWPS.
+
+
+The third option in the Script type combobox exports an actinia process
+chain (non-parameterized model) or an actinia template (parameterized model).
+An actinia JSON based on the model will be generated then; as for the PyWPS
+script, the Run button is disabled as users are expected to include this
+JSON in their web processing service and not to run it on itself.
+
+
+
+
+
+Figure: Script editor in the wxGUI Graphical Modeler - set to actinia.
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png
new file mode 100644
index 00000000000..099fcf968c4
Binary files /dev/null and b/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png differ
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_items.png b/gui/wxpython/gmodeler/g_gui_gmodeler_items.png
index 976bb125cb9..146a45e89a1 100644
Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_items.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_items.png differ
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python.png
index 6eb219bc475..9c07efd7606 100644
Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python.png differ
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png
index 77c28876d9c..1cf220fdc53 100644
Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png differ
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png
index 777ba420027..75db847b647 100644
Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png differ
diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png
index 0c5acf5f402..e62da627f20 100644
Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png differ
diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py
index b33c5b6d4e4..00f670d20cb 100644
--- a/gui/wxpython/gmodeler/model.py
+++ b/gui/wxpython/gmodeler/model.py
@@ -17,6 +17,7 @@
- model::ModelComment
- model::ProcessModelFile
- model::WriteModelFile
+ - model::WriteActiniaFile
- model::WritePyWPSFile
- model::WritePythonFile
- model::ModelParamDialog
@@ -27,7 +28,7 @@
(>=v2). Read the file COPYING that comes with GRASS for details.
@author Martin Landa
-@PyWPS, Python parameterization Ondrej Pesek
+@actinia, PyWPS, Python parameterization Ondrej Pesek
"""
import os
@@ -2636,6 +2637,7 @@ def __init__(self, fd, model):
self.fd = None
self.model = None
self.indent = None
+ self.grassAPI = None
# call method_write...()
@@ -2701,12 +2703,17 @@ def _writePythonComment(self, item):
for line in item.GetLabel().splitlines():
self.fd.write("#" + line + "\n")
+ def _getParamName(self, parameter_name, item):
+ return "{module_nickname}_{param_name}".format(
+ module_nickname=self._getModuleNickname(item),
+ param_name=parameter_name,
+ )
+
@staticmethod
- def _getParamName(parameter_name, item):
- return "{module_name}{module_id}_{param_name}".format(
+ def _getModuleNickname(item):
+ return "{module_name}{module_id}".format(
module_name=re.sub("[^a-zA-Z]+", "", item.GetLabel()),
module_id=item.GetId(),
- param_name=parameter_name,
)
def _getItemFlags(self, item, opts, variables):
@@ -2743,6 +2750,155 @@ def _getItemFlags(self, item, opts, variables):
return item_true_flags, item_parameterized_flags, item_params
+class WriteActiniaFile(WriteScriptFile):
+ """Class for exporting model to an actinia script."""
+
+ def __init__(self, fd, model, grassAPI=None):
+ """Class for exporting model to actinia script."""
+ self.fd = fd
+ self.model = model
+ self.indent = 2
+
+ self._writeActinia()
+
+ def _writeActinia(self):
+ """Write actinia model to file."""
+ properties = self.model.GetProperties()
+
+ description = properties["description"]
+
+ self.fd.write(
+ f"""{{
+{' ' * self.indent * 1}"id": "model",
+{' ' * self.indent * 1}"description": "{'""'.join(description.splitlines())}",
+{' ' * self.indent * 1}"version": "1",
+"""
+ )
+
+ parameterized = False
+ module_list_str = ""
+ for item in self.model.GetItems(ModelAction):
+ parameterizedParams = item.GetParameterizedParams()
+ if len(parameterizedParams["params"]) > 0:
+ parameterized = True
+
+ module_list_str += self._getPythonAction(item, parameterizedParams)
+ module_list_str += f"{' ' * self.indent * 3}}},\n"
+
+ if parameterized is True:
+ self.fd.write(f'{" " * self.indent * 1}"template": {{\n')
+ self.fd.write(
+ f"""{' ' * self.indent * 2}"list": [
+ """
+ )
+ else:
+ self.fd.write(
+ f"""{' ' * self.indent}"list": [
+ """
+ )
+
+ # module_list_str[:-2] to get rid of the trailing comma and newline
+ self.fd.write(module_list_str[:-2] + "\n")
+
+ if parameterized is True:
+ self.fd.write(f"{' ' * self.indent * 2}]\n{' ' * self.indent * 1}}}\n}}")
+ else:
+ self.fd.write(f"{' ' * self.indent * 1}]\n}}")
+
+ def _getPythonAction(self, item, variables={}, intermediates=None):
+ """Write model action to Python file"""
+ task = GUI(show=None).ParseCommand(cmd=item.GetLog(string=False))
+ strcmd = f"{' ' * self.indent * 3}{{\n"
+
+ return (
+ strcmd + self._getPythonActionCmd(item, task, len(strcmd), variables) + "\n"
+ )
+
+ def _getPythonActionCmd(self, item, task, cmdIndent, variables={}):
+ opts = task.get_options()
+
+ ret = ""
+ parameterizedParams = [v["name"] for v in variables["params"]]
+
+ flags, itemParameterizedFlags, params = self._getItemFlags(
+ item, opts, variables
+ )
+ inputs = []
+ outputs = []
+
+ if len(itemParameterizedFlags) > 0:
+ dlg = wx.MessageDialog(
+ self.model.canvas,
+ message=_(
+ f"Module {task.get_name()} in your model contains "
+ f"parameterized flags. actinia does not support "
+ f"parameterized flags. The following flags are therefore "
+ f"not being written in the generated json: "
+ f"{itemParameterizedFlags}"
+ ),
+ caption=_("Warning"),
+ style=wx.OK_DEFAULT | wx.ICON_WARNING,
+ )
+ dlg.ShowModal()
+ dlg.Destroy()
+
+ for p in opts["params"]:
+ name = p.get("name", None)
+ value = p.get("value", None)
+
+ if (name and value) or (name in parameterizedParams):
+
+ if name in parameterizedParams:
+ parameterizedParam = self._getParamName(name, item)
+ default_val = p.get("value", "")
+
+ if len(default_val) > 0:
+ parameterizedParam += f"|default({default_val})"
+
+ value = f"{{{{ {parameterizedParam} }}}}"
+
+ param_string = f'{{"param": "{name}", "value": "{value}"}}'
+ age = p.get("age", "old")
+ if age == "new":
+ outputs.append(param_string)
+ else:
+ inputs.append(param_string)
+
+ ret += f'{" " * self.indent * 4}"module": "{task.get_name()}",\n'
+ ret += f'{" " * self.indent * 4}"id": "{self._getModuleNickname(item)}",\n'
+
+ # write flags
+ if flags:
+ ret += f'{" " * self.indent * 4}"flags": "{flags}",\n'
+
+ # write inputs and outputs
+ if len(inputs) > 0:
+ ret += self.write_params("inputs", inputs)
+ else:
+ ret += "}"
+
+ if len(outputs) > 0:
+ ret += self.write_params("outputs", outputs)
+
+ # ret[:-2] to get rid of the trailing comma and newline
+ # (to make the json valid)
+ return ret[:-2]
+
+ def write_params(self, param_type, params):
+ """Write the full list of parameters of one type.
+
+ :param param_type: type of parameters (inputs or outputs)
+ :params: list of the parameters
+ """
+ ret = f'{" " * self.indent * 4}"{param_type}": [\n'
+ for opt in params[:-1]:
+ ret += f"{' ' * self.indent * 5}{opt},\n"
+ ret += f"{' ' * self.indent * 5}{params[-1]}\n"
+ ret += f"{' ' * self.indent * 4}],\n"
+
+ return ret
+
+
class WritePyWPSFile(WriteScriptFile):
"""Class for exporting model to PyWPS script."""
@@ -2786,7 +2942,7 @@ def __init__(self):
""" # noqa: E501
)
- for item in self.model.GetItems():
+ for item in self.model.GetItems(ModelAction):
self._write_input_outputs(item, self.model.GetIntermediateData()[:3])
self.fd.write(
diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py
index 76a5237448e..7a2762ab4ce 100644
--- a/gui/wxpython/gmodeler/panels.py
+++ b/gui/wxpython/gmodeler/panels.py
@@ -75,6 +75,7 @@
WriteModelFile,
ModelDataSeries,
ModelDataSingle,
+ WriteActiniaFile,
WritePythonFile,
WritePyWPSFile,
)
@@ -185,7 +186,7 @@ def __init__(
page=self.variablePanel, text=_("Variables"), name="variables"
)
self.notebook.AddPage(
- page=self.pythonPanel, text=_("Python editor"), name="python"
+ page=self.pythonPanel, text=_("Script editor"), name="python"
)
self.notebook.AddPage(
page=self.goutput, text=_("Command output"), name="output"
@@ -1036,8 +1037,25 @@ def OnExportImage(self, event):
dlg.Destroy()
def OnExportPython(self, event=None, text=None):
- """Export model to Python script"""
+ """Export model to Python script."""
+ self.pythonPanel.SetWriteObject("Python")
+ self.ExportScript()
+
+ def OnExportPyWPS(self, event=None, text=None):
+ """Export model to PyWPS script."""
+ self.pythonPanel.SetWriteObject("PyWPS")
+ self.ExportScript()
+
+ def OnExportActinia(self, event=None, text=None):
+ """Export model to actinia script."""
+ self.pythonPanel.SetWriteObject("actinia")
+ self.ExportScript()
+
+ def ExportScript(self):
+ """Export model to script."""
+ orig_script_type = self.pythonPanel.body.script_type
filename = self.pythonPanel.SaveAs(force=True)
+ self.pythonPanel.SetWriteObject(orig_script_type)
self.SetStatusText(_("Model exported to <%s>") % filename)
def OnPreferences(self, event):
@@ -1606,6 +1624,7 @@ def __init__(self, parent, id=wx.ID_ANY, **kwargs):
choices=[
_("Python"),
_("PyWPS"),
+ _("actinia"),
],
)
self.script_type_box.SetSelection(0) # Python
@@ -1644,6 +1663,30 @@ def _layout(self):
sizer.SetSizeHints(self)
self.SetSizer(sizer)
+ def GetScriptExt(self):
+ """Get extension for script exporting.
+ :return: script extension
+ """
+ if self.write_object == WriteActiniaFile:
+ ext = "json"
+ else:
+ # Python, PyWPS
+ ext = "py"
+
+ return ext
+
+ def SetWriteObject(self, script_type):
+ """Set correct self.write_object dependng on the script type.
+ :param script_type: script type name as a string
+ """
+ if script_type == "PyWPS":
+ self.write_object = WritePyWPSFile
+ elif script_type == "actinia":
+ self.write_object = WriteActiniaFile
+ else:
+ # script_type == "Python", fallback
+ self.write_object = WritePythonFile
+
def RefreshScript(self):
"""Refresh the script.
@@ -1693,12 +1736,18 @@ def SaveAs(self, force=False):
:return: filename
"""
filename = ""
+ file_ext = self.GetScriptExt()
+ if file_ext == "py":
+ fn_wildcard = _("Python script (*.py)|*.py")
+ elif file_ext == "json":
+ fn_wildcard = _("JSON file (*.json)|*.json")
+
dlg = wx.FileDialog(
parent=self,
message=_("Choose file to save"),
defaultFile=os.path.basename(self.parent.GetModelFile(ext=False)),
defaultDir=os.getcwd(),
- wildcard=_("Python script (*.py)|*.py"),
+ wildcard=fn_wildcard,
style=wx.FD_SAVE,
)
@@ -1709,8 +1758,8 @@ def SaveAs(self, force=False):
return ""
# check for extension
- if filename[-3:] != ".py":
- filename += ".py"
+ if filename[-len(file_ext) - 1 :] != f".{file_ext}":
+ filename += f".{file_ext}"
if os.path.exists(filename):
dlg = wx.MessageDialog(
@@ -1780,10 +1829,8 @@ def OnDone(self, event):
def OnChangeScriptType(self, event):
new_script_type = self.script_type_box.GetStringSelection()
- if new_script_type == "Python":
- self.write_object = WritePythonFile
- elif new_script_type == "PyWPS":
- self.write_object = WritePyWPSFile
+
+ self.SetWriteObject(new_script_type)
if self.RefreshScript():
self.body.script_type = new_script_type
@@ -1795,11 +1842,9 @@ def OnChangeScriptType(self, event):
self.script_type_box.SetStringSelection(self.body.script_type)
if self.body.script_type == "Python":
- self.write_object = WritePythonFile
self.btnRun.Enable()
self.btnRun.SetToolTip(_("Run script"))
- elif self.body.script_type == "PyWPS":
- self.write_object = WritePyWPSFile
+ elif self.body.script_type in ("PyWPS", "actinia"):
self.btnRun.Disable()
self.btnRun.SetToolTip(
_("Run script - enabled only for basic Python scripts")
diff --git a/gui/wxpython/xml/menudata_modeler.xml b/gui/wxpython/xml/menudata_modeler.xml
index cf6d1b3e444..9102b524541 100644
--- a/gui/wxpython/xml/menudata_modeler.xml
+++ b/gui/wxpython/xml/menudata_modeler.xml
@@ -43,6 +43,16 @@
OnExportPythonCtrl+Alt+P
+
+