Skip to content

Commit f9d8ce1

Browse files
authored
Merge pull request #46 from dragos-ana/set-user-4C-schema
Add 4C schema specification mechanism
2 parents 0229d43 + 463ac5b commit f9d8ce1

File tree

5 files changed

+96
-53
lines changed

5 files changed

+96
-53
lines changed

src/fourc_webviewer/cli_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def get_arguments():
2727
parser.add_argument(
2828
"--fourc_yaml_file", type=str, help="input file path to visualize"
2929
)
30+
parser.add_argument(
31+
"--fourc_schema_file",
32+
type=str,
33+
help="path to schema file to be used for validation (optional: if not specified, the schema from fourcipp is used)",
34+
)
3035
parser.add_argument(
3136
"--server",
3237
action="store_true",

src/fourc_webviewer/fourc_webserver.py

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
web viewer."""
44

55
import copy
6+
import json
67
import re
78
import shutil
89
import tempfile
@@ -55,18 +56,33 @@ class FourCWebServer:
5556
def __init__(
5657
self,
5758
fourc_yaml_file,
59+
fourc_schema_file=None,
5860
page_title="4C Webviewer",
5961
):
6062
"""Constructor.
6163
6264
Args:
6365
fourc_yaml_file (string|Path): path to the input fourc yaml file.
66+
fourc_schema_file (string|Path): path to the fourc schema json file. If not provided, the default FourCIPP schema from CONFIG.fourc_json_schema is used.
6467
page_title (string): page title appearing in the browser
6568
tab.
6669
"""
6770

6871
self.server = get_server()
6972

73+
# set 4C schema: if a file was provided, read it; otherwise take the schema provided by fourcipp
74+
if fourc_schema_file:
75+
# check whether the provided file is really a json schema file
76+
if Path(fourc_schema_file).resolve().suffix != ".json":
77+
raise ValueError(
78+
f"The provided schema file {Path(fourc_schema_file)} is not a valid json file!"
79+
)
80+
# read schema
81+
with open(fourc_schema_file, "r", encoding="utf-8") as f:
82+
self.state.fourc_json_schema = json.load(f)
83+
else:
84+
self.state.fourc_json_schema = CONFIG.fourc_json_schema
85+
7086
# initialize include upload value: False (bottom sheet with include upload is not displayed until there is a fourcyaml file uploaded)
7187
self.state.include_upload_open = False
7288
self.state.included_files = []
@@ -103,7 +119,10 @@ def __init__(
103119
self._server_vars["fourc_yaml_size"],
104120
self._server_vars["fourc_yaml_last_modified"],
105121
self._server_vars["fourc_yaml_read_in_status"],
106-
) = read_fourc_yaml_file(fourc_yaml_file)
122+
) = read_fourc_yaml_file(
123+
fourc_yaml_file=fourc_yaml_file,
124+
fourc_json_schema=self.state.fourc_json_schema,
125+
)
107126

108127
if self._server_vars["fourc_yaml_read_in_status"]:
109128
self.state.read_in_status = self.state.all_read_in_statuses["success"]
@@ -329,21 +348,27 @@ def init_pyvista_render_objects(self):
329348
# get mesh of the selected material
330349
self._actors["material_meshes"] = {}
331350
for material in self.state.materials_section.keys():
332-
# get meshes of materials
351+
# get mesh of specific material
333352
master_mat_ind = self.determine_master_mat_ind_for_material(material)
334-
335-
self._actors["material_meshes"][material] = self._server_vars[
336-
"render_window"
337-
].add_mesh(
338-
problem_mesh.threshold(
339-
value=(master_mat_ind - 0.05, master_mat_ind + 0.05),
340-
scalars="element-material",
341-
),
342-
color="darkorange",
343-
opacity=0.7,
344-
render=False,
353+
master_mat_mesh = problem_mesh.threshold(
354+
value=(master_mat_ind - 0.05, master_mat_ind + 0.05),
355+
scalars="element-material",
345356
)
346357

358+
# determine whether this material has no assigned nodes
359+
if len(master_mat_mesh.points) == 0:
360+
self._actors["material_meshes"][material] = None
361+
362+
else:
363+
self._actors["material_meshes"][material] = self._server_vars[
364+
"render_window"
365+
].add_mesh(
366+
master_mat_mesh,
367+
color="darkorange",
368+
opacity=0.7,
369+
render=False,
370+
)
371+
347372
all_dc_entities = [
348373
{"entity": k, "geometry_type": sec_name}
349374
for sec_name, sec in self.state.dc_sections.items()
@@ -441,16 +466,21 @@ def update_pyvista_render_objects(self):
441466
].SetVisibility(True)
442467
legend_items.append(("Selected design condition", "navy"))
443468

444-
for mat in self._actors.get("material_meshes", {}).values():
445-
mat.SetVisibility(False)
469+
for mat_mesh in self._actors.get("material_meshes", {}).values():
470+
if (
471+
mat_mesh is not None
472+
): # set visibility as false for existing material meshes
473+
mat_mesh.SetVisibility(False)
446474
if (
447475
self.state.selected_material
448476
and self.state.selected_main_section_name == "MATERIALS"
449477
):
450-
self._actors["material_meshes"][self.state.selected_material].SetVisibility(
451-
True
452-
)
453-
legend_items.append(("Selected material", "orange"))
478+
# only visualize material if it really has an assigned mesh
479+
if self._actors["material_meshes"][self.state.selected_material]:
480+
self._actors["material_meshes"][
481+
self.state.selected_material
482+
].SetVisibility(True)
483+
legend_items.append(("Selected material", "orange"))
454484

455485
self._server_vars["render_window"].remove_legend()
456486
if legend_items:
@@ -475,8 +505,6 @@ def init_general_sections_state_and_server_vars(self):
475505
approach to add them up to the main section SOLVERS.
476506
"""
477507

478-
self.state.json_schema = CONFIG.fourc_json_schema
479-
480508
# define substrings of section names to exclude
481509
substr_to_exclude = [
482510
"DESIGN",
@@ -1166,7 +1194,10 @@ def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs):
11661194
self._server_vars["fourc_yaml_size"],
11671195
self._server_vars["fourc_yaml_last_modified"],
11681196
self._server_vars["fourc_yaml_read_in_status"],
1169-
) = read_fourc_yaml_file(temp_fourc_yaml_file)
1197+
) = read_fourc_yaml_file(
1198+
fourc_yaml_file=temp_fourc_yaml_file,
1199+
fourc_json_schema=self.state.fourc_json_schema,
1200+
)
11701201
self._server_vars["fourc_yaml_name"] = Path(temp_fourc_yaml_file).name
11711202

11721203
if self._server_vars["fourc_yaml_read_in_status"]:
@@ -1249,7 +1280,7 @@ def change_export_fourc_yaml_path(self, export_fourc_yaml_path, **kwargs):
12491280
def click_delete_section_button(self, **kwargs):
12501281
"""Deletes the currently selected section; if it was the last
12511282
subsection, delete the main too."""
1252-
if self.state.selected_section_name in self.state.json_schema.get(
1283+
if self.state.selected_section_name in self.state.fourc_json_schema.get(
12531284
"required", []
12541285
):
12551286
return
@@ -1298,7 +1329,7 @@ def change_add_section(self, **kwargs):
12981329
add_section = self.state.add_section
12991330
main_section_name = add_section.split("/")[0] or ""
13001331

1301-
if add_section not in self.state.json_schema.get("properties", {}):
1332+
if add_section not in self.state.fourc_json_schema.get("properties", {}):
13021333
return
13031334

13041335
general_sections = copy.deepcopy(self.state.general_sections) or {}
@@ -1551,7 +1582,9 @@ def click_save_button(self, **kwargs):
15511582

15521583
# dump content to the defined export file
15531584
self._server_vars["fourc_yaml_file_write_status"] = write_fourc_yaml_file(
1554-
self._server_vars["fourc_yaml_content"], self.state.export_fourc_yaml_path
1585+
fourc_yaml_content=self._server_vars["fourc_yaml_content"],
1586+
new_fourc_yaml_file=self.state.export_fourc_yaml_path,
1587+
fourc_json_schema=self.state.fourc_json_schema,
15551588
)
15561589

15571590
# check write status
@@ -1570,7 +1603,7 @@ def on_sections_change(self, general_sections, **kwargs):
15701603

15711604
dict_leaves_to_number_if_schema(fourcinput._sections)
15721605

1573-
fourcinput.validate()
1606+
fourcinput.validate(json_schema=self.state.fourc_json_schema)
15741607
self.state.input_error_dict = {}
15751608
except ValidationError as exc:
15761609
self.state.input_error_dict = parse_validation_error_text(

src/fourc_webviewer/gui_utils.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ def _top_row(server):
648648
vuetify.VAutocomplete(
649649
v_model=("add_section",),
650650
items=(
651-
"Object.keys(json_schema['properties']).filter(key => !new Set(['MATERIALS', 'TITLE', 'CLONING MATERIAL MAP', 'RESULT DESCRIPTION']).has(key) && !(['DESIGN', 'TOPOLOGY', 'ELEMENTS', 'NODE', 'FUNCT', 'GEOMETRY'].some(n => key.includes(n))))",
651+
"Object.keys(fourc_json_schema['properties']).filter(key => !new Set(['MATERIALS', 'TITLE', 'CLONING MATERIAL MAP', 'RESULT DESCRIPTION']).has(key) && !(['DESIGN', 'TOPOLOGY', 'ELEMENTS', 'NODE', 'FUNCT', 'GEOMETRY'].some(n => key.includes(n))))",
652652
),
653653
dense=True,
654654
solo=True,
@@ -689,7 +689,7 @@ def _prop_value_table(server):
689689
):
690690
with html.Td(classes="text-center pa-0", style="position: relative;"):
691691
with vuetify.VBtn(
692-
v_if="edit_mode == all_edit_modes['edit_mode'] && !json_schema['properties']?.[selected_section_name]?.['required']?.includes(item_key)",
692+
v_if="edit_mode == all_edit_modes['edit_mode'] && !fourc_json_schema['properties']?.[selected_section_name]?.['required']?.includes(item_key)",
693693
tag="a",
694694
v_bind="{...props, target: '_blank'}",
695695
click=(server.controller.delete_row, "[item_key]"),
@@ -709,7 +709,7 @@ def _prop_value_table(server):
709709
html.Span(v_text=("item_key",), v_bind="props")
710710
html.P(
711711
v_text=(
712-
"json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['description'] || 'no description'",
712+
"fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['description'] || 'no description'",
713713
),
714714
style="max-width: 450px;",
715715
)
@@ -729,10 +729,10 @@ def _prop_value_table(server):
729729
"general_sections[selected_main_section_name][selected_section_name][item_key]", # binding item_val directly does not work, since Object.entries(...) creates copies for the mutable objects
730730
),
731731
v_if=(
732-
"(json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'string' "
733-
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'number' "
734-
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'integer')"
735-
"&& !json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['enum']"
732+
"(fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'string' "
733+
"|| fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'number' "
734+
"|| fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'integer')"
735+
"&& !fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['enum']"
736736
),
737737
blur=server.controller.on_leave_edit_field,
738738
update_modelValue="flushState('general_sections')", # this is required in order to flush the state changes correctly to the server, as our passed on v-model is a nested variable
@@ -747,7 +747,7 @@ def _prop_value_table(server):
747747
# if item is a boolean -> use VSwitch
748748
with html.Div(
749749
v_if=(
750-
"json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] === 'boolean'"
750+
"fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] === 'boolean'"
751751
),
752752
classes="d-flex align-center justify-center",
753753
):
@@ -769,13 +769,13 @@ def _prop_value_table(server):
769769
"[selected_section_name][item_key]"
770770
),
771771
v_if=(
772-
"json_schema['properties']?.[selected_section_name]"
772+
"fourc_json_schema['properties']?.[selected_section_name]"
773773
"?.['properties']?.[item_key]?.['enum']"
774774
),
775775
update_modelValue="flushState('general_sections')",
776776
# bind the enum array as items
777777
items=(
778-
"json_schema['properties'][selected_section_name]['properties'][item_key]['enum']",
778+
"fourc_json_schema['properties'][selected_section_name]['properties'][item_key]['enum']",
779779
),
780780
dense=True,
781781
solo=True,
@@ -812,7 +812,7 @@ def _prop_value_table(server):
812812
update_modelValue="flushState('general_sections')",
813813
# bind the enum array as items
814814
items=(
815-
"Object.keys(json_schema['properties']?.[selected_section_name]?.['properties'])",
815+
"Object.keys(fourc_json_schema['properties']?.[selected_section_name]?.['properties'])",
816816
),
817817
dense=True,
818818
solo=True,
@@ -831,10 +831,10 @@ def _prop_value_table(server):
831831
vuetify.VTextField(
832832
v_model=("add_value",),
833833
v_if=(
834-
"(json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'string' "
835-
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'number' "
836-
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'integer')"
837-
"&& !json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['enum']"
834+
"(fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'string' "
835+
"|| fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'number' "
836+
"|| fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'integer')"
837+
"&& !fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['enum']"
838838
),
839839
update_modelValue="flushState('add_value')", # this is required in order to flush the state changes correctly to the server, as our passed on v-model is a nested variable
840840
classes="w-80 pb-1",
@@ -849,7 +849,7 @@ def _prop_value_table(server):
849849
# if item is a boolean -> use VSwitch
850850
with html.Div(
851851
v_if=(
852-
"json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] === 'boolean'"
852+
"fourc_json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] === 'boolean'"
853853
),
854854
classes="d-flex align-center justify-center",
855855
):
@@ -865,13 +865,13 @@ def _prop_value_table(server):
865865
vuetify.VAutocomplete(
866866
v_model=("add_value"),
867867
v_if=(
868-
"json_schema['properties']?.[selected_section_name]"
868+
"fourc_json_schema['properties']?.[selected_section_name]"
869869
"?.['properties']?.[add_key]?.['enum']"
870870
),
871871
update_modelValue="flushState('add_value')",
872872
# bind the enum array as items
873873
items=(
874-
"json_schema['properties'][selected_section_name]['properties'][add_key]['enum']",
874+
"fourc_json_schema['properties'][selected_section_name]['properties'][add_key]['enum']",
875875
),
876876
dense=True,
877877
solo=True,
@@ -931,7 +931,7 @@ def _materials_panel():
931931
classes="ga-3 mb-5 pl-5 pr-5 w-full",
932932
v_if=("edit_mode == all_edit_modes['view_mode']",),
933933
v_text=(
934-
"json_schema?.properties?.MATERIALS?.items?.oneOf?"
934+
"fourc_json_schema?.properties?.MATERIALS?.items?.oneOf?"
935935
".find(v => v.properties?.[materials_section[selected_material]?.TYPE])?.properties?"
936936
".[materials_section[selected_material]?.TYPE]?.description || 'Error on material description'",
937937
),
@@ -996,7 +996,7 @@ def _materials_panel():
996996
html.P(v_text=("param_key",), v_bind="props")
997997
html.P(
998998
v_text=(
999-
"json_schema?.properties?.MATERIALS?.items?.oneOf?"
999+
"fourc_json_schema?.properties?.MATERIALS?.items?.oneOf?"
10001000
".find(v => v.properties?.[materials_section[selected_material]?.TYPE])?"
10011001
".properties?.[materials_section[selected_material]?.TYPE]?.properties?"
10021002
".[param_key]?.description || 'Error on parameter description'",
@@ -1511,7 +1511,7 @@ def create_gui(server, render_window):
15111511
outlined=True,
15121512
color="red",
15131513
v_if=(
1514-
"!json_schema['required'].includes(selected_section_name) && Object.keys(general_sections).includes(selected_main_section_name)",
1514+
"!fourc_json_schema['required'].includes(selected_section_name) && Object.keys(general_sections).includes(selected_main_section_name)",
15151515
),
15161516
click=server.controller.click_delete_section_button,
15171517
)

0 commit comments

Comments
 (0)