From 91efe4a48833500bbbe4b3b12e9dd99b78c3461c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Kr=C3=B6ber?= Date: Wed, 1 May 2024 21:28:17 +0200 Subject: [PATCH 1/4] feat: blockly visualisation of models :gift: --- demo/mapping.ipynb | 27 +- demo/recipes.ipynb | 25 +- semantique/mapping.py | 13 +- semantique/recipe.py | 13 +- semantique/visualiser/__init__.py | 0 semantique/visualiser/blockdefs.json | 2433 ++++++++++++++++++++++++++ semantique/visualiser/convert.py | 446 +++++ semantique/visualiser/model_vis.html | 539 ++++++ semantique/visualiser/visualise.py | 84 + 9 files changed, 3570 insertions(+), 10 deletions(-) create mode 100644 semantique/visualiser/__init__.py create mode 100644 semantique/visualiser/blockdefs.json create mode 100644 semantique/visualiser/convert.py create mode 100644 semantique/visualiser/model_vis.html create mode 100644 semantique/visualiser/visualise.py diff --git a/demo/mapping.ipynb b/demo/mapping.ipynb index 575d573d..0208b236 100644 --- a/demo/mapping.ipynb +++ b/demo/mapping.ipynb @@ -429,7 +429,26 @@ "id": "98511bd7", "metadata": {}, "source": [ - "The flexible structure with the building blocks of semantique make many more structures possible. Now you have an idea of how to construct a mapping from scratch using the built-in Semantique configuration, we move on and construct a complete mapping in one go. However, we use simpler rules as above, since our demo EO data cube only contains a very limited set of resources." + "The flexible structure with the building blocks of semantique make many more structures possible. Note that you can visualise the structure of your defined mapping by calling `.visualise()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9ee5654", + "metadata": {}, + "outputs": [], + "source": [ + "mapping.visualise()" + ] + }, + { + "cell_type": "markdown", + "id": "21de8ad5", + "metadata": {}, + "source": [ + "\n", + "Now you have an idea of how to construct a mapping from scratch using the built-in Semantique configuration, we move on and construct a complete mapping in one go. However, we use simpler rules as above, since our demo EO data cube only contains a very limited set of resources." ] }, { @@ -1078,9 +1097,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:semantique]", + "display_name": "gsemantique", "language": "python", - "name": "conda-env-semantique-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1092,7 +1111,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.10.1" } }, "nbformat": 4, diff --git a/demo/recipes.ipynb b/demo/recipes.ipynb index 8e90cb9c..0dd56da1 100644 --- a/demo/recipes.ipynb +++ b/demo/recipes.ipynb @@ -200,7 +200,24 @@ "id": "4dedbc77", "metadata": {}, "source": [ - "The recipe we just constructred looks like [this](https://github.com/ZGIS/semantique/blob/main/demo/files/recipe.json).\n", + "The recipe we just constructred looks like [this](https://github.com/ZGIS/semantique/blob/main/demo/files/recipe.json). It can be visualised by calling the corresponding method as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab0596d2", + "metadata": {}, + "outputs": [], + "source": [ + "recipe.visualise()" + ] + }, + { + "cell_type": "markdown", + "id": "6e47eb13", + "metadata": {}, + "source": [ "\n", "## Setting the context\n", "\n", @@ -1863,9 +1880,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:semantique]", + "display_name": "gsemantique", "language": "python", - "name": "conda-env-semantique-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1877,7 +1894,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.10.1" } }, "nbformat": 4, diff --git a/semantique/mapping.py b/semantique/mapping.py index f354cc90..47681092 100644 --- a/semantique/mapping.py +++ b/semantique/mapping.py @@ -4,6 +4,7 @@ from semantique.processor.core import QueryProcessor from semantique.processor.arrays import Collection from semantique.processor import reducers +from semantique.visualiser.visualise import show class Mapping(dict): """Base class for mapping configurations. @@ -159,4 +160,14 @@ def translate(self, *reference, property = None, extent, datacube, ) out = processor.call_handler(property) out.name = reference[-1] - return out \ No newline at end of file + return out + + def visualise(self): + """Visualise the mapping rules in a web browser. + + This method visualises the mapping rules of the mapping instance in a web + browser. The visualisation is based on Blockly, a web-based visual programming + editor. The mapping rules are converted into Blockly XML format and served + to the browser. + """ + show(self) \ No newline at end of file diff --git a/semantique/recipe.py b/semantique/recipe.py index 9a005da9..9a4fb4a1 100644 --- a/semantique/recipe.py +++ b/semantique/recipe.py @@ -1,4 +1,5 @@ from semantique.processor.core import QueryProcessor +from semantique.visualiser.visualise import show class QueryRecipe(dict): """Dict-like container to store instructions of a query recipe. @@ -99,4 +100,14 @@ def execute(self, datacube, mapping, space, time, run_preview = False, else: # Execute the query recipe without a preview run. qp = QueryProcessor.parse(self, datacube, mapping, space, time, **config) - return qp.optimize().execute() \ No newline at end of file + return qp.optimize().execute() + + def visualise(self): + """Visualise the recipe in a web browser. + + This method visualises the recipe in a web browser. + The visualisation is based on Blockly, a web-based visual programming + editor. The recipe is converted into Blockly XML format and served + to the browser. + """ + show(self) \ No newline at end of file diff --git a/semantique/visualiser/__init__.py b/semantique/visualiser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/semantique/visualiser/blockdefs.json b/semantique/visualiser/blockdefs.json new file mode 100644 index 00000000..f8aada0a --- /dev/null +++ b/semantique/visualiser/blockdefs.json @@ -0,0 +1,2433 @@ +[ + { + "type": "model_root", + "message0": "name %1 %2 concepts %3 application %4", + "args0": [ + { + "type": "field_input", + "name": "name", + "text": "< model >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "concepts", + "check": "concept_definition" + }, + { + "type": "input_statement", + "name": "application", + "check": "result_definition" + } + ], + "inputsInline": true, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "model_root_a", + "message0": "name %1 %2 concepts %3", + "args0": [ + { + "type": "field_input", + "name": "name", + "text": "< model >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "concepts", + "check": "concept_definition" + } + ], + "inputsInline": true, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "model_root_b", + "message0": "name %1 %2 application %3", + "args0": [ + { + "type": "field_input", + "name": "name", + "text": "< model >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "application", + "check": "result_definition" + } + ], + "inputsInline": true, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "entity", + "message0": "entity %1 name %2 %3 properties %4", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "field_input", + "name": "name", + "text": "< entity >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "item_0", + "check": "property_definition" + } + ], + "previousStatement": [ + "concept_definition", + "entity_definition", + "definition" + ], + "nextStatement": [ + "concept_definition", + "entity_definition", + "definition" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "property", + "message0": "property %1 name %2 %3 rules %4", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "field_input", + "name": "name", + "text": "< property >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "rules", + "check": "array" + } + ], + "output": [ + "property_definition", + "definition" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "result", + "message0": "result %1 name %2 %3 instructions %4 export %5", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "field_input", + "name": "name", + "text": "< result >" + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "instructions", + "check": "array" + }, + { + "type": "field_dropdown", + "name": "export", + "options": [ + [ + "yes", + "true" + ], + [ + "no", + "false" + ] + ] + } + ], + "previousStatement": [ + "result_definition", + "definition" + ], + "nextStatement": [ + "result_definition", + "definition" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "entity_reference", + "message0": "entity %1", + "args0": [ + { + "type": "field_input", + "name": "name", + "text": "< name >" + } + ], + "output": [ + "entity", + "concept", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "property_reference", + "message0": "entity %1 %2 property %3", + "args0": [ + { + "type": "field_input", + "name": "entity_name", + "text": "< name >" + }, + { + "type": "input_dummy" + }, + { + "type": "field_input", + "name": "property_name", + "text": "< name >" + } + ], + "inputsInline": true, + "output": [ + "property", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "result_reference", + "message0": "result %1", + "args0": [ + { + "type": "field_input", + "name": "name", + "text": "< name >" + } + ], + "output": [ + "result", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "self_reference", + "message0": "self", + "output": [ + "self", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "appearance", + "message0": "appearance %1", + "args0": [ + { + "type": "field_input", + "name": "measurement", + "dimension": "< measurement >" + } + ], + "output": [ + "appearance", + "layer", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "atmosphere", + "message0": "atmosphere %1", + "args0": [ + { + "type": "field_input", + "name": "measurement", + "dimension": "< measurement >" + } + ], + "output": [ + "atmosphere", + "layer", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "artifacts", + "message0": "artifact %1", + "args0": [ + { + "type": "field_input", + "name": "measurement", + "dimension": "< measurement >" + } + ], + "output": [ + "artifacts", + "layer", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "reflectance", + "message0": "reflectance %1", + "args0": [ + { + "type": "field_input", + "name": "measurement", + "dimension": "< measurement >" + } + ], + "output": [ + "reflectance", + "layer", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "topography", + "message0": "topography %1", + "args0": [ + { + "type": "field_input", + "name": "measurement", + "dimension": "< measurement >" + } + ], + "output": [ + "topography", + "layer", + "reference", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "collection", + "message0": "collection %1 %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "item_0", + "check": "array" + } + ], + "output": "collection", + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "processing_chain", + "message0": "with %1 do %2", + "args0": [ + { + "type": "input_value", + "name": "with", + "check": [ + "array", + "collection" + ] + }, + { + "type": "input_value", + "name": "do", + "check": [ + "verb", + "verb_chain" + ] + } + ], + "inputsInline": false, + "output": [ + "processing_chain", + "array" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "verb_chain", + "message0": "chain %1 %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "item_0", + "check": "verb" + } + ], + "inputsInline": false, + "output": "verb_chain", + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "evaluate_univariate", + "message0": "evaluate %1", + "args0": [ + { + "type": "field_dropdown", + "name": "operator", + "options": [ + [ + "not", + "not" + ], + [ + "absolute", + "absolute" + ], + [ + "floor", + "floor" + ], + [ + "ceiling", + "ceiling" + ], + [ + "natural logarithm", + "natural_logarithm" + ], + [ + "exponential", + "exponential" + ], + [ + "square root", + "square_root" + ], + [ + "cube root", + "cube_root" + ], + [ + "is missing", + "is_missing" + ], + [ + "not missing", + "not_missing" + ], + [ + "cosine", + "cosine" + ], + [ + "sine", + "sine" + ], + [ + "tangent", + "tangent" + ], + [ + "secant", + "secant" + ], + [ + "cosecant", + "cosecant" + ], + [ + "cotangent", + "cotangent" + ], + [ + "to degrees", + "to_degrees" + ], + [ + "to radians", + "to_radians" + ] + ] + } + ], + "inputsInline": false, + "output": [ + "evaluate", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "evaluate_bivariate", + "message0": "evaluate %1 %2 %3", + "args0": [ + { + "type": "field_dropdown", + "name": "operator", + "options": [ + [ + "+", + "add" + ], + [ + "-", + "subtract" + ], + [ + "*", + "multiply" + ], + [ + "/", + "divide" + ], + [ + "^", + "power" + ], + [ + "=", + "equal" + ], + [ + "≠", + "not_equal" + ], + [ + ">", + "greater" + ], + [ + "<", + "less" + ], + [ + "≥", + "greater_equal" + ], + [ + "≤", + "less_equal" + ], + [ + "in", + "in" + ], + [ + "not in", + "not_in" + ], + [ + "and", + "and" + ], + [ + "or", + "or" + ], + [ + "exclusive or", + "exclusive_or" + ], + [ + "intersects", + "intersects" + ], + [ + "after", + "after" + ], + [ + "before", + "before" + ], + [ + "during", + "during" + ], + [ + "normalized difference", + "normalized_difference" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "y", + "check": [ + "array", + "value", + "set", + "interval" + ] + } + ], + "inputsInline": true, + "output": [ + "evaluate", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "extract", + "message0": "extract %1", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + } + ], + "inputsInline": true, + "output": [ + "extract", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "extract_time", + "message0": "extract time %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "year", + "year" + ], + [ + "season", + "season" + ], + [ + "quarter", + "quarter" + ], + [ + "month", + "month" + ], + [ + "week", + "week" + ], + [ + "day", + "day" + ], + [ + "day of the year", + "day_of_year" + ], + [ + "day of the week", + "day_of_week" + ], + [ + "hour", + "hour" + ], + [ + "minute", + "minute" + ], + [ + "second", + "second" + ], + [ + "datetime", + "null" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "extract", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "extract_space", + "message0": "extract space %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "feature", + "feature" + ], + [ + "x", + "x" + ], + [ + "y", + "y" + ], + [ + "(x, y)", + "null" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "extract", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "filter", + "message0": "filter %1", + "args0": [ + { + "type": "input_value", + "name": "filterer", + "check": "array" + } + ], + "inputsInline": false, + "output": [ + "filter", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "filter_time", + "message0": "filter time %1 %2 %3 %4", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "year", + "year" + ], + [ + "season", + "season" + ], + [ + "quarter", + "quarter" + ], + [ + "month", + "month" + ], + [ + "week", + "week" + ], + [ + "day", + "day" + ], + [ + "day of the year", + "day_of_year" + ], + [ + "day of the week", + "day_of_week" + ], + [ + "hour", + "hour" + ], + [ + "minute", + "minute" + ], + [ + "second", + "second" + ], + [ + "datetime", + "null" + ] + ] + }, + { + "type": "field_dropdown", + "name": "operator", + "options": [ + [ + "=", + "equal" + ], + [ + "≠", + "not_equal" + ], + [ + ">", + "greater" + ], + [ + "<", + "less" + ], + [ + "≥", + "greater_equal" + ], + [ + "≤", + "less_equal" + ], + [ + "in", + "in" + ], + [ + "after", + "after" + ], + [ + "before", + "before" + ], + [ + "during", + "during" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "y", + "check": [ + "value", + "value_list", + "value_range" + ] + } + ], + "inputsInline": true, + "output": [ + "filter", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "filter_space", + "message0": "filter space %1 %2 %3 %4", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "feature", + "feature" + ], + [ + "x", + "x" + ], + [ + "y", + "y" + ], + [ + "(x, y)", + "null" + ] + ] + }, + { + "type": "field_dropdown", + "name": "operator", + "options": [ + [ + "=", + "equal" + ], + [ + "≠", + "not_equal" + ], + [ + ">", + "greater" + ], + [ + "<", + "less" + ], + [ + "≥", + "greater_equal" + ], + [ + "≤", + "less_equal" + ], + [ + "in", + "in" + ], + [ + "intersects", + "intersects" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "y", + "check": [ + "value", + "value_list", + "value_range" + ] + } + ], + "inputsInline": true, + "output": [ + "filter", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "assign", + "message0": "assign %1", + "args0": [ + { + "type": "input_value", + "name": "y", + "check": [ + "array", + "value" + ] + } + ], + "inputsInline": false, + "output": [ + "assign", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "assign_time", + "message0": "assign time %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "year", + "year" + ], + [ + "season", + "season" + ], + [ + "quarter", + "quarter" + ], + [ + "month", + "month" + ], + [ + "week", + "week" + ], + [ + "day", + "day" + ], + [ + "day of the year", + "day_of_year" + ], + [ + "day of the week", + "day_of_week" + ], + [ + "hour", + "hour" + ], + [ + "minute", + "minute" + ], + [ + "second", + "second" + ], + [ + "datetime", + "null" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "assign", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "assign_space", + "message0": "assign space %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "feature", + "feature" + ], + [ + "x", + "x" + ], + [ + "y", + "y" + ], + [ + "(x, y)", + "null" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "assign", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "assign_at", + "message0": "assign %1 at %2", + "args0": [ + { + "type": "input_value", + "name": "y", + "check": [ + "array", + "value" + ] + }, + { + "type": "input_value", + "name": "at", + "check": "array" + } + ], + "inputsInline": true, + "output": [ + "assign", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "groupby", + "message0": "group by %1", + "args0": [ + { + "type": "input_value", + "name": "grouper", + "check": [ + "array", + "collection" + ] + } + ], + "inputsInline": false, + "output": [ + "groupby", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "groupby_time", + "message0": "group by time %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "year", + "year" + ], + [ + "season", + "season" + ], + [ + "quarter", + "quarter" + ], + [ + "month", + "month" + ], + [ + "week", + "week" + ], + [ + "day", + "day" + ], + [ + "day of the year", + "day_of_year" + ], + [ + "day of the week", + "day_of_week" + ], + [ + "hour", + "hour" + ], + [ + "minute", + "minute" + ], + [ + "second", + "second" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "groupby", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "groupby_space", + "message0": "group by space %1", + "args0": [ + { + "type": "field_dropdown", + "name": "component", + "options": [ + [ + "feature", + "feature" + ], + [ + "x", + "x" + ], + [ + "y", + "y" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "groupby", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "reduce", + "message0": "reduce over %1 %2 using %3", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "reduce", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "reduce_spacetime", + "message0": "reduce over %1 %2 using %3", + "args0": [ + { + "type": "field_dropdown", + "name": "dimension", + "options": [ + [ + "time", + "TIME" + ], + [ + "space", + "SPACE" + ], + [ + "x", + "X" + ], + [ + "y", + "Y" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "reduce", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "reduce_all", + "message0": "reduce using %1", + "args0": [ + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "reduce", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "shift", + "message0": "shift %1 steps along %2", + "args0": [ + { + "type": "field_number", + "name": "steps", + "value": 1 + }, + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + } + ], + "inputsInline": true, + "output": [ + "shift", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "shift_spacetime", + "message0": "shift %1 %2 steps along %3", + "args0": [ + { + "type": "field_number", + "name": "steps", + "value": 1 + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "dimension", + "options": [ + [ + "time", + "TIME" + ], + [ + "space", + "SPACE" + ], + [ + "x", + "X" + ], + [ + "y", + "Y" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "shift", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "smooth", + "message0": "smooth %1 %2 using %3 %4 on size %5", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "field_number", + "name": "size", + "value": 1, + "min": 0 + } + ], + "inputsInline": true, + "output": [ + "smooth", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "smooth_spacetime", + "message0": "smooth %1 %2 using %3 %4 on size %5", + "args0": [ + { + "type": "field_dropdown", + "name": "dimension", + "options": [ + [ + "time", + "TIME" + ], + [ + "space", + "SPACE" + ], + [ + "x", + "X" + ], + [ + "y", + "Y" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "field_number", + "name": "size", + "value": 1, + "min": 0 + } + ], + "inputsInline": true, + "output": [ + "smooth", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "trim", + "message0": "trim %1", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + } + ], + "inputsInline": true, + "output": [ + "trim", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "trim_spacetime", + "message0": "trim %1", + "args0": [ + { + "type": "field_dropdown", + "name": "dimension", + "options": [ + [ + "time", + "TIME" + ], + [ + "space", + "SPACE" + ], + [ + "x", + "X" + ], + [ + "y", + "Y" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "trim", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "trim_all", + "message0": "trim", + "inputsInline": true, + "output": [ + "trim", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "delineate", + "message0": "delineate", + "inputsInline": true, + "output": [ + "delineate", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "compose", + "message0": "compose", + "output": [ + "compose", + "collection_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "concatenate", + "message0": "concatenate along %1", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + } + ], + "inputsInline": false, + "output": [ + "concatenate", + "collection_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "merge", + "message0": "merge using %1", + "args0": [ + { + "type": "field_dropdown", + "name": "reducer", + "options": [ + [ + "all", + "all" + ], + [ + "any", + "any" + ], + [ + "none", + "none" + ], + [ + "count", + "count" + ], + [ + "percentage", + "percentage" + ], + [ + "min", + "min" + ], + [ + "max", + "max" + ], + [ + "mean", + "mean" + ], + [ + "median", + "median" + ], + [ + "mode", + "mode" + ], + [ + "product", + "product" + ], + [ + "sum", + "sum" + ], + [ + "n", + "n" + ], + [ + "range", + "range" + ], + [ + "standard deviation", + "standard_deviation" + ], + [ + "variance", + "variance" + ], + [ + "first", + "first" + ], + [ + "last", + "last" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "merge", + "collection_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "name", + "message0": "name as %1", + "args0": [ + { + "type": "field_input", + "name": "value", + "text": "< newname >" + } + ], + "inputsInline": true, + "output": [ + "name", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "set", + "message0": "set %1 %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "item_0", + "check": "value" + } + ], + "output": "set", + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "interval", + "message0": "interval from %1 to %2", + "args0": [ + { + "type": "field_number", + "name": "a", + "value": 0, + "precision": 0.01 + }, + { + "type": "field_number", + "name": "b", + "value": 1, + "precision": 0.01 + } + ], + "output": [ + "interval", + "set" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "label_dropdown", + "message0": "label %1", + "args0": [ + { + "type": "field_input", + "name": "label", + "dimension": "< label >" + } + ], + "output": [ + "label_dropdown", + "label", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "label_text", + "message0": "label %1", + "args0": [ + { + "type": "field_input", + "name": "x", + "text": "< label >" + } + ], + "output": [ + "label_text", + "label", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "character", + "message0": "character %1", + "args0": [ + { + "type": "field_input", + "name": "value", + "text": "< text >" + } + ], + "output": [ + "character", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "number", + "message0": "number %1", + "args0": [ + { + "type": "field_number", + "name": "x", + "value": 0, + "precision": 0.01 + } + ], + "output": [ + "number", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "boolean", + "message0": "boolean %1", + "args0": [ + { + "type": "field_dropdown", + "name": "value", + "options": [ + [ + "true", + "true" + ], + [ + "false", + "false" + ] + ] + } + ], + "output": [ + "boolean", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "time_instant", + "message0": "time instant %1", + "args0": [ + { + "type": "field_input", + "name": "x", + "text": "< time >" + } + ], + "output": [ + "time_instant", + "time", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "time_interval", + "message0": "time interval %1 %2", + "args0": [ + { + "type": "field_input", + "name": "a", + "text": "< from >" + }, + { + "type": "field_input", + "name": "b", + "text": "< to >" + } + ], + "output": [ + "time_interval", + "time", + "value" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "comment", + "message0": "# %1", + "args0": [ + { + "type": "field_input", + "name": "comment", + "text": "< comment >" + } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": 230, + "tooltip": "Use this block for adding a comment", + "helpUrl": "" + }, + { + "type": "fill", + "message0": "fill along %1 %2 using %3", + "args0": [ + { + "type": "field_input", + "name": "dimension", + "text": "< dimension >" + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "method", + "options": [ + [ + "nearest", + "nearest" + ], + [ + "linear", + "linear" + ], + [ + "cubic", + "cubic" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "fill", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "fill_spacetime", + "message0": "fill along %1 %2 using %3", + "args0": [ + { + "type": "field_dropdown", + "name": "dimension", + "options": [ + [ + "time", + "TIME" + ], + [ + "space", + "SPACE" + ], + [ + "x", + "X" + ], + [ + "y", + "Y" + ] + ] + }, + { + "type": "input_dummy" + }, + { + "type": "field_dropdown", + "name": "method", + "options": [ + [ + "nearest", + "nearest" + ], + [ + "linear", + "linear" + ], + [ + "cubic", + "cubic" + ] + ] + } + ], + "inputsInline": true, + "output": [ + "fill", + "single_verb", + "verb" + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + } +] \ No newline at end of file diff --git a/semantique/visualiser/convert.py b/semantique/visualiser/convert.py new file mode 100644 index 00000000..b9922187 --- /dev/null +++ b/semantique/visualiser/convert.py @@ -0,0 +1,446 @@ +import numpy as np +import uuid +import xml.etree.ElementTree as ET +from copy import deepcopy + + +class JsonToXmlConverter: + def __init__(self): + pass + + def convert(self, json_concept=None, json_app=None): + """ + Converts a JSONs as used by semantique to an XML string that can be parsed by Blockly. + Parsable input JSONs can represent a concept defintion (mapping.py) and/or + an application definition (recipe.py). + """ + # define the base layout of the XML + root = ET.Element("xml") + model_args = dict(id=str(uuid.uuid4()), deletable="false", x="10", y="10") + if json_concept and json_app: + model_root = ET.SubElement(root, "block", type="model_root", **model_args) + elif json_concept: + model_root = ET.SubElement(root, "block", type="model_root_a", **model_args) + elif json_app: + model_root = ET.SubElement(root, "block", type="model_root_b", **model_args) + ET.SubElement(model_root, "field", name="name").text = "Semantic model" + if json_concept: + self.convert_concepts(model_root, json_concept) + if json_app: + self.convert_app(model_root, json_app) + return ET.tostring(root) + + def convert_concepts(self, parent, obj): + """ + Parses the concepts part of the model, i.e. the definition of entities + by means of their properties. + """ + concepts = ET.SubElement(parent, "statement", name="concepts") + prev_entity = None + for _, entity in enumerate(obj["entity"]): + if prev_entity is None: + prev_entity = ET.SubElement( + concepts, "block", type="entity", id=self._gen_id() + ) + else: + next_tag = ET.SubElement(prev_entity, "next") + prev_entity = ET.SubElement( + next_tag, "block", type="entity", id=self._gen_id() + ) + entity_def = obj["entity"][entity] + mutation = ET.Element("mutation") + mutation.attrib["listlength"] = str(len(entity_def) - 1) + prev_entity.append(mutation) + ET.SubElement(prev_entity, "field", name="name").text = entity + for index, prop in enumerate(entity_def): + prev_prop = ET.SubElement(prev_entity, "value", name=f"item_{index}") + prop_block = ET.SubElement( + prev_prop, "block", type="property", id=self._gen_id() + ) + ET.SubElement(prop_block, "field", name="name").text = prop + value_block = ET.SubElement(prop_block, "value", name="rules") + self.find_handler(value_block, entity_def[prop]) + + def convert_app(self, parent, obj): + """ + Parses the application part of the model, i.e. the definition of the + processing chain. + """ + application = ET.SubElement(parent, "statement", name="application") + prev_result = None + for key, value in obj.items(): + if prev_result is None: + prev_result = ET.SubElement( + application, "block", type="result", id=self._gen_id() + ) + else: + next_tag = ET.SubElement(prev_result, "next") + prev_result = ET.SubElement( + next_tag, "block", type="result", id=self._gen_id() + ) + ET.SubElement(prev_result, "field", name="name").text = key + ET.SubElement(prev_result, "field", name="export").text = "true" + instructs = ET.SubElement(prev_result, "value", name="instructions") + instructs_block = ET.SubElement( + instructs, "block", type=value["type"], id=self._gen_id() + ) + self.handle_with(instructs_block, value["with"]) + self.handle_do(instructs_block, value["do"]) + + def find_handler(self, parent, obj): + """Calls the dedicated handler for a building block.""" + try: + handler = getattr(self, "handle_" + obj["type"]) + except AttributeError: + raise Exception(f"No handler found for type {obj['type']}.") + return handler(parent, obj) + + def handle_apply_custom(self, parent, obj): + """ + Handles custom verbs that are not part of the standard set of verbs. + Note that custom operators are handled via handle_evaluate and + custom reducers are handled via handle_reduce. + """ + block = ET.SubElement( + parent, "block", type=obj["params"]["verb"], id=self._gen_id() + ) + custom_props = deepcopy(obj["params"]) + custom_props.pop("verb", None) + ET.SubElement(block, "field", name="custom_props").text = ", ".join( + [f"{key} = {str(value)}" for key, value in custom_props.items()] + ) + + def handle_assign(self, parent, obj): + if "at" in obj["params"]: + block = ET.SubElement(parent, "block", type="assign_at", id=self._gen_id()) + value = ET.SubElement(block, "value", name="y") + if isinstance(obj["params"]["y"], dict): + self.find_handler(value, obj["params"]["y"]) + else: + self.handle_value(value, obj["params"]["y"]) + value = ET.SubElement(block, "value", name="at") + if isinstance(obj["params"]["at"], dict): + self.find_handler(value, obj["params"]["at"]) + else: + self.handle_value(value, obj["params"]["at"]) + else: + shortcut = self.handle_shortcut(parent, obj, "assign", "y") + if not shortcut: + block = ET.SubElement(parent, "block", type="assign", id=self._gen_id()) + value = ET.SubElement(block, "value", name="y") + if isinstance(obj["params"]["y"], dict): + self.find_handler(value, obj["params"]["y"]) + else: + self.handle_value(value, obj["params"]["y"]) + + def handle_collection(self, parent, obj): + block = ET.SubElement(parent, "block", type="collection", id=self._gen_id()) + mutation = ET.Element("mutation") + mutation.attrib["listlength"] = str(len(obj["elements"]) - 1) + block.append(mutation) + for index, item in enumerate(obj["elements"]): + value = ET.SubElement(block, "value", name=f"item_{index}") + if isinstance(item, dict): + self.find_handler(value, item) + elif isinstance(item, str): + self.handle_entity(value, {"elements": [item]}) + else: + raise Exception("Unknown item type in collection elements") + + def handle_compose(self, parent, obj): + ET.SubElement(parent, "block", type="compose", id=self._gen_id()) + + def handle_concatenate(self, parent, obj): + block = ET.SubElement(parent, "block", type="concatenate", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + + def handle_concept(self, parent, obj): + ref = obj["reference"][0] + try: + handler = getattr(self, "handle_" + ref) + except AttributeError: + raise Exception(f"No handler found for reference {ref}.") + return handler(parent, obj) + + def handle_do(self, parent, do_obj): + do_value = ET.SubElement(parent, "value", name="do") + if len(do_obj) == 1: + self.find_handler(do_value, do_obj[0]) + else: + verb_block = ET.SubElement( + do_value, "block", type="verb_chain", id=self._gen_id() + ) + mutation = ET.Element("mutation") + mutation.attrib["listlength"] = str(len(do_obj) - 1) + verb_block.append(mutation) + for index, action in enumerate(do_obj): + value = ET.SubElement(verb_block, "value", name=f"item_{index}") + self.find_handler(value, action) + + def handle_delineate(self, parent, obj): + ET.SubElement(parent, "block", type="delineate", id=self._gen_id()) + + def handle_entity(self, parent, obj): + block = ET.SubElement( + parent, "block", type="entity_reference", id=self._gen_id() + ) + for ref in obj["reference"][1:]: + ET.SubElement(block, "field", name="name").text = ref + + def handle_evaluate(self, parent, obj): + if "y" in obj["params"]: + block = ET.SubElement( + parent, "block", type="evaluate_bivariate", id=self._gen_id() + ) + ET.SubElement(block, "field", name="operator").text = obj["params"][ + "operator" + ] + value = ET.SubElement(block, "value", name="y") + if isinstance(obj["params"]["y"], dict): + # algebraic, boolean or membership relationships need to be handled + self.find_handler(value, obj["params"]["y"]) + else: + # algebraic relationships with single value are handled + self.handle_value(value, obj["params"]["y"]) + else: + block = ET.SubElement( + parent, "block", type="evaluate_univariate", id=self._gen_id() + ) + ET.SubElement(block, "field", name="operator").text = obj["params"][ + "operator" + ] + + def handle_extract(self, parent, obj): + if "component" in obj["params"]: + if obj["params"]["dimension"] == "time": + block = ET.SubElement( + parent, "block", type="extract_time", id=self._gen_id() + ) + elif obj["params"]["dimension"] == "space": + block = ET.SubElement( + parent, "block", type="extract_space", id=self._gen_id() + ) + ET.SubElement(block, "field", name="component").text = obj["params"][ + "component" + ] + else: + block = ET.SubElement(parent, "block", type="extract", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + + def handle_fill(self, parent, obj): + if obj["params"]["dimension"] in ["time", "space"]: + block = ET.SubElement( + parent, "block", type="fill_spacetime", id=self._gen_id() + ) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ].upper() + else: + block = ET.SubElement(parent, "block", type="fill", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + ET.SubElement(block, "field", name="method").text = str(obj["params"]["method"]) + + def handle_filter(self, parent, obj): + block = ET.SubElement(parent, "block", type="filter", id=self._gen_id()) + value_block = ET.SubElement(block, "value", name="filterer") + self.find_handler(value_block, obj["params"]["filterer"]) + + def handle_geometry(self, parent, obj): + block = ET.SubElement(parent, "block", type="geometry", id=self._gen_id()) + ET.SubElement(block, "field", name="measurement").text = ( + f"{obj['content']['type']} with {len(obj['content']['features'])} features" + ) + + def handle_groupby(self, parent, obj): + shortcut = self.handle_shortcut(parent, obj, "groupby", "grouper") + if not shortcut: + block = ET.SubElement(parent, "block", type="groupby", id=self._gen_id()) + value_block = ET.SubElement(block, "value", name="grouper") + self.find_handler(value_block, obj["params"]["grouper"]) + + def handle_interval(self, parent, obj): + block = ET.SubElement(parent, "block", type="interval", id=self._gen_id()) + ET.SubElement(block, "field", name="a").text = str(obj["content"][0]) + ET.SubElement(block, "field", name="b").text = str(obj["content"][1]) + + def handle_layer(self, parent, obj): + lyr = obj["reference"] + block = ET.SubElement(parent, "block", type=lyr[-2], id=self._gen_id()) + ET.SubElement(block, "field", name="measurement").text = lyr[-1] + + def handle_merge(self, parent, obj): + block = ET.SubElement(parent, "block", type="merge", id=self._gen_id()) + ET.SubElement(block, "field", name="reducer").text = obj["params"]["reducer"] + + def handle_name(self, parent, obj): + block = ET.SubElement(parent, "block", type="name", id=self._gen_id()) + ET.SubElement(block, "field", name="value").text = obj["params"]["value"] + + def handle_processing_chain(self, parent, obj): + block = ET.SubElement(parent, "block", type=obj["type"], id=self._gen_id()) + self.handle_with(block, obj["with"]) + self.handle_do(block, obj["do"]) + + def handle_reduce(self, parent, obj): + if "dimension" in obj["params"]: + if obj["params"]["dimension"] in ["time", "space"]: + block = ET.SubElement( + parent, "block", type="reduce_spacetime", id=self._gen_id() + ) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ].upper() + else: + block = ET.SubElement(parent, "block", type="reduce", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + else: + block = ET.SubElement(parent, "block", type="reduce_all", id=self._gen_id()) + ET.SubElement(block, "field", name="reducer").text = obj["params"]["reducer"] + + def handle_result(self, parent, obj): + block = ET.SubElement( + parent, "block", type="result_reference", id=self._gen_id() + ) + ET.SubElement(block, "field", name="name").text = obj["name"] + + def handle_self(self, parent, obj): + ET.SubElement(parent, "block", type="self_reference", id=self._gen_id()) + + def handle_set(self, parent, obj): + block = ET.SubElement(parent, "block", type="set", id=self._gen_id()) + mutation = ET.Element("mutation") + mutation.attrib["listlength"] = str(len(obj["content"]) - 1) + block.append(mutation) + for index, item in enumerate(obj["content"]): + value = ET.SubElement(block, "value", name=f"item_{index}") + self.handle_value(value, item) + + def handle_shift(self, parent, obj): + block = ET.SubElement( + parent, "block", type="shift_spacetime", id=self._gen_id() + ) + ET.SubElement(block, "field", name="steps").text = str(obj["params"]["steps"]) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ].upper() + + def handle_shortcut(self, parent, obj, block_type, param_name): + """ + Evaluates if shortcut is possible for a given block type and parameter name. + Shortcut representations are those that are structured as _. + These shortcut representations are used to simplify the processing chain. + """ + shortcut = False + try: + params = obj["params"][param_name] + if ( + params["type"] == "processing_chain" + and params["with"]["type"] == "self" + and len(params["do"]) == 1 + and params["do"][0]["type"] == "verb" + and params["do"][0]["name"] == "extract" + and "dimension" in params["do"][0]["params"] + and "component" in params["do"][0]["params"] + and params["do"][0]["params"]["dimension"] in ["time", "space"] + ): + block = ET.SubElement( + parent, + "block", + type=f"{block_type}_{params['do'][0]['params']['dimension']}", + id=self._gen_id(), + ) + value = ET.SubElement(block, "field", name="component") + value.text = params["do"][0]["params"]["component"] + shortcut = True + except (KeyError, TypeError): + pass + return shortcut + + def handle_smooth(self, parent, obj): + if obj["params"]["dimension"] in ["time", "space"]: + block = ET.SubElement( + parent, "block", type="smooth_spacetime", id=self._gen_id() + ) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ].upper() + else: + block = ET.SubElement(parent, "block", type="smooth", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + ET.SubElement(block, "field", name="reducer").text = obj["params"]["reducer"] + ET.SubElement(block, "field", name="size").text = str(obj["params"]["size"]) + + def handle_time_instant(self, parent, obj): + block = ET.SubElement(parent, "block", type="time_instant", id=self._gen_id()) + ET.SubElement(block, "field", name="x").text = obj["content"]["start"] + + def handle_time_interval(self, parent, obj): + block = ET.SubElement(parent, "block", type="time_interval", id=self._gen_id()) + ET.SubElement(block, "field", name="a").text = obj["content"]["start"] + ET.SubElement(block, "field", name="b").text = obj["content"]["end"] + + def handle_trim(self, parent, obj): + if "dimension" in obj["params"]: + if obj["params"]["dimension"] in ["time", "space"]: + block = ET.SubElement( + parent, "block", type="trim_spacetime", id=self._gen_id() + ) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ].upper() + else: + block = ET.SubElement(parent, "block", type="trim", id=self._gen_id()) + ET.SubElement(block, "field", name="dimension").text = obj["params"][ + "dimension" + ] + else: + block = ET.SubElement(parent, "block", type="trim_all", id=self._gen_id()) + + def handle_with(self, parent, with_obj): + value = ET.SubElement(parent, "value", name="with") + self.find_handler(value, with_obj) + + def handle_value(self, parent, obj): + """ + Note that there is an ambiguity in handling values since there is no + m:1 mapping between dtypes and value types as used by semantique. + * String values can be mapped to character, label_text + * Numeric values can be mapped to number, label + """ + if isinstance(obj, bool): + block = ET.SubElement(parent, "block", type="boolean", id=self._gen_id()) + ET.SubElement(block, "field", name="value").text = str(obj).lower() + elif isinstance(obj, str): + block = ET.SubElement(parent, "block", type="character", id=self._gen_id()) + ET.SubElement(block, "field", name="value").text = str(obj) + elif isinstance(obj, (int, float)): + if np.isnan(obj): + block = ET.SubElement( + parent, "block", type="is_missing", id=self._gen_id() + ) + ET.SubElement(block, "field", name="measurement").text = "nan" + else: + block = ET.SubElement(parent, "block", type="number", id=self._gen_id()) + ET.SubElement(block, "field", name="x").text = str(obj) + else: + raise ValueError(f"No handler found for value {obj}") + + def handle_verb(self, parent, obj): + try: + handler = getattr(self, "handle_" + obj["name"]) + except AttributeError: + raise Exception(f"No handler found for type {obj['name']}.") + return handler(parent, obj) + + def _gen_id(self): + return str(uuid.uuid4()) diff --git a/semantique/visualiser/model_vis.html b/semantique/visualiser/model_vis.html new file mode 100644 index 00000000..947844fb --- /dev/null +++ b/semantique/visualiser/model_vis.html @@ -0,0 +1,539 @@ + + + + + Blockly Visualization + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/semantique/visualiser/visualise.py b/semantique/visualiser/visualise.py new file mode 100644 index 00000000..0843e539 --- /dev/null +++ b/semantique/visualiser/visualise.py @@ -0,0 +1,84 @@ +import os +import tempfile +import threading +import uuid +import webbrowser +from http.server import HTTPServer, SimpleHTTPRequestHandler +from semantique.visualiser.convert import JsonToXmlConverter + + +def show(json_model, out_path=None): + """ + Visualises semantic model in a web browser by converting it to XML + and rendering it as a Blockly model. Auto-infers which parts of the model + are contained (i.e. concepts and/or application parts). + + Args: + json_model (dict): JSON model to visualise + out_path (str): path to save the converted Blockly XML file, default is None + """ + # auto-infer model parts + json_concept, json_app = None, None + if "concepts" in json_model.keys(): + json_concept = json_model["concepts"] + if "application" in json_model.keys(): + json_app = json_model["application"] + if json_concept is None and json_app is None: + if len(json_model.keys()) == 1 and "entity" in json_model.keys(): + json_concept = json_model + else: + json_app = json_model + + # convert JSON model to XML + converter = JsonToXmlConverter() + xml = converter.convert(json_concept=json_concept, json_app=json_app) + + # optionally write to out_path + if out_path: + with open(out_path, "w") as f: + f.write(xml.decode("utf-8")) + + # visualise the xml + render_xml(xml) + + +def render_xml(xml): + """ + Renders Blockly XML in a web browser + + Args: + xml (str): Blockly XML to visualise + """ + # create temp directory + tmpdir = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex) + os.makedirs(tmpdir) + + # write converted xml to temp file + xml_path = os.path.join(tmpdir, "output.xml") + with open(xml_path, "w") as f: + f.write(xml.decode("utf-8")) + + # copy relevant files to be served to temp directory + files = ["model_vis.html", "blockdefs.json"] + for file in files: + with open(os.path.join(os.path.dirname(__file__), file), "r") as f: + with open(os.path.join(tmpdir, file), "w") as temp_f: + temp_f.write(f.read()) + + # start a Python server to serve the HTML file + class HTTPHandler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=tmpdir, **kwargs) + + def log_message(self, format, *args): + pass + + server = HTTPServer(("localhost", 0), HTTPHandler) + thread = threading.Thread(target=server.serve_forever) + thread.start() + + # open the HTML file in a web browser + xml_path = os.path.split(xml_path)[-1] + address_I, address_II = server.server_address[0], server.server_address[1] + url = f"http://{address_I}:{address_II}/model_vis.html?xml={xml_path}" + webbrowser.open(url) From c6e234fd0ad456a346a06f133954fcc86af045ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Kr=C3=B6ber?= Date: Wed, 15 May 2024 09:44:38 +0200 Subject: [PATCH 2/4] deps: Html/json support files as package_data :couple: --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 13d1f00b..398b6707 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,9 @@ author = "Lucas van der Meer", author_email = "lucas.vandermeer@sbg.ac.at", packages = find_packages(), + package_data={ + "semantique.visualiser": ["*.html", "*.json"], + }, python_requires = ">=3.9", install_requires = dependencies ) \ No newline at end of file From d377e20657132e4a4ca78074cf7f9d628ceba26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Kr=C3=B6ber?= Date: Fri, 28 Jun 2024 13:53:35 +0200 Subject: [PATCH 3/4] fix: render custom verbs with no args :wrench: --- semantique/visualiser/convert.py | 7 ++++--- semantique/visualiser/model_vis.html | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/semantique/visualiser/convert.py b/semantique/visualiser/convert.py index b9922187..78198133 100644 --- a/semantique/visualiser/convert.py +++ b/semantique/visualiser/convert.py @@ -106,9 +106,10 @@ def handle_apply_custom(self, parent, obj): ) custom_props = deepcopy(obj["params"]) custom_props.pop("verb", None) - ET.SubElement(block, "field", name="custom_props").text = ", ".join( - [f"{key} = {str(value)}" for key, value in custom_props.items()] - ) + if len(custom_props): + ET.SubElement(block, "field", name="custom_props").text = ", ".join( + [f"{key} = {str(value)}" for key, value in custom_props.items()] + ) def handle_assign(self, parent, obj): if "at" in obj["params"]: diff --git a/semantique/visualiser/model_vis.html b/semantique/visualiser/model_vis.html index 947844fb..8dacb8ae 100644 --- a/semantique/visualiser/model_vis.html +++ b/semantique/visualiser/model_vis.html @@ -453,6 +453,9 @@ "text": "< custom_props >" }); blockDef["helpUrl"] = "reduce"; + } else if (fieldName === "custom_props" && numArgs === 0) { + blockDef["inputsInline"] = true; + blockDef["helpUrl"] = "trim_all"; } else { console.warn( `Unknown block of...\n` + @@ -517,8 +520,13 @@ } // Check for custom verbs or measurements if (!Blockly.Blocks[blockType]) { - const fieldName = allBlocks[i].getElementsByTagName('field')[0].getAttribute('name'); - const numArgs = allBlocks[i].getElementsByTagName('field').length; + let fieldName = "custom_props"; + let numArgs = 0; + try { + fieldName = allBlocks[i].getElementsByTagName('field')[0].getAttribute('name'); + numArgs = allBlocks[i].getElementsByTagName('field').length; + } catch (error) { + } const fallbackBlockDef = createBlockDef(blockType, fieldName, numArgs); Blockly.defineBlocksWithJsonArray([fallbackBlockDef]); } From f0cdbd07cf1351a33dfc82daf3eecf342e66bcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Kr=C3=B6ber?= Date: Fri, 28 Jun 2024 13:55:30 +0200 Subject: [PATCH 4/4] fix: replace hardcoded with-do with find_handler :wrench: --- semantique/visualiser/convert.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/semantique/visualiser/convert.py b/semantique/visualiser/convert.py index 78198133..78e51bbd 100644 --- a/semantique/visualiser/convert.py +++ b/semantique/visualiser/convert.py @@ -81,11 +81,7 @@ def convert_app(self, parent, obj): ET.SubElement(prev_result, "field", name="name").text = key ET.SubElement(prev_result, "field", name="export").text = "true" instructs = ET.SubElement(prev_result, "value", name="instructions") - instructs_block = ET.SubElement( - instructs, "block", type=value["type"], id=self._gen_id() - ) - self.handle_with(instructs_block, value["with"]) - self.handle_do(instructs_block, value["do"]) + self.find_handler(instructs, value) def find_handler(self, parent, obj): """Calls the dedicated handler for a building block."""