From e55f2dd90b0af1036e5e368006304523fdf158d4 Mon Sep 17 00:00:00 2001 From: Fernando Bordignon Date: Wed, 7 Aug 2024 15:37:58 -0300 Subject: [PATCH] v2.4.0 --- .gitmodules | 3 - pyproject.toml | 11 +- src/ltrace/ltrace/algorithms/detect_cups.py | 21 +- .../algorithms/supervised/mrcnn/utils.py | 5 +- .../CoreEnv/.gitkeep | 0 .../ImageLogEnv/.gitkeep | 0 .../ImageLogEnv/synth_side.h5} | 0 .../public/MicroCTEnv/.gitkeep} | 0 .../public/ThinSectionEnv/.gitkeep} | 0 .../unet-binary-segop.h5 | 0 .../assets/trained_models/ca-bundle.crt | 3772 ----------------- src/ltrace/ltrace/assets_utils.py | 58 +- src/ltrace/ltrace/constants.py | 11 + src/ltrace/ltrace/file_utils.py | 3 - .../image/core_box/core_boxes_image_file.py | 4 +- src/ltrace/ltrace/image/las.py | 230 + src/ltrace/ltrace/image/lasGeologFile.py | 330 ++ src/ltrace/ltrace/pore_networks/functions.py | 90 +- .../generalized_network_extractor.py | 2 +- .../simulation_parameters_node.py | 14 +- .../pore_networks/visualization_model.py | 15 +- .../ltrace/readers/microtom/collector.py | 21 +- src/ltrace/ltrace/remote/hosts/utils.py | 2 +- src/ltrace/ltrace/remote/jobs.py | 33 +- src/ltrace/ltrace/screenshot/Screenshot.py | 157 +- src/ltrace/ltrace/slicer/cache/cache_files.py | 2 + src/ltrace/ltrace/slicer/cli_utils.py | 35 +- .../ltrace/slicer/color_map_customizer.py | 3 +- .../ltrace/slicer/custom_export_to_file.py | 90 + src/ltrace/ltrace/slicer/data_utils.py | 48 +- src/ltrace/ltrace/slicer/equations/schema.py | 65 - src/ltrace/ltrace/slicer/graph_data.py | 64 +- src/ltrace/ltrace/slicer/helpers.py | 142 +- .../ltrace/slicer/lazy/protocols/base.py | 2 +- src/ltrace/ltrace/slicer/node_observer.py | 9 +- src/ltrace/ltrace/slicer/project_manager.py | 184 +- .../slicer/side_by_side_image_layout.py | 54 +- .../ltrace/slicer/tests/ltrace_plugin_test.py | 154 +- src/ltrace/ltrace/slicer/tests/test_case.py | 16 +- src/ltrace/ltrace/slicer/tests/utils.py | 41 +- src/ltrace/ltrace/slicer/tracking/tracker.py | 23 - .../tracking/trackers/module_tracker.py | 13 - .../tracking/trackers/volume_node_tracker.py | 70 - .../tracking/trackers/widget_tracker.py | 561 --- .../slicer/tracking/tracking_manager.py | 62 - src/ltrace/ltrace/slicer/undo.py | 6 +- .../slicer/widget/hierarchy_volume_input.py | 6 + .../ltrace/slicer/widget/histogram_popup.py | 2 +- src/ltrace/ltrace/slicer/widgets.py | 68 +- src/ltrace/ltrace/slicer_utils.py | 89 +- .../ltrace/utils/CorrelatedLabelMapVolume.py | 27 +- src/ltrace/ltrace/utils/__init__.py | 1 + src/ltrace/ltrace/utils/blob2hash.py | 9 + src/ltrace/ltrace/workflow/Workflow.py | 6 + .../workflow/workstep/data/ExportLAS.py | 111 + .../workstep/data/ThinSectionLoader.py | 2 +- .../ltrace/workflow/workstep/data/__init__.py | 1 + .../workstep/segmentation/InspectorIslands.py | 92 + .../segmentation/InspectorWatershed.py | 11 +- .../workstep/segmentation/ThinSectionPores.py | 55 + .../workstep/segmentation/__init__.py | 2 + .../simulation/InputTablesListWidget.py | 161 + .../simulation/PoreNetworkExtractor.py | 28 +- .../simulation/PoreNetworkSimMercury.py | 81 + .../simulation/PoreNetworkSimOnePhase.py | 49 +- .../simulation/PoreNetworkSimTwoPhase.py | 29 +- .../workflow/workstep/simulation/__init__.py | 1 + src/ltrace/ltrace/wrappers.py | 108 +- src/ltrace/requirements.txt | 19 +- .../SegmentEditorEffect.py | 56 +- src/modules/Charts/Charts.py | 93 +- .../Charts/Plots/Crossplot/CrossplotWidget.py | 843 ++-- .../Plots/Crossplot/data_plot_widget.py | 36 +- .../SegmentEditorEffect.py | 17 +- .../SegmentEditorEffect.py | 25 +- src/modules/CoreEnv/CoreEnv.py | 4 +- .../CoreGeometryCLI/CoreGeometryCLI.py | 11 +- .../CoreImagesImport/CoreImagesImport.py | 7 +- src/modules/CoreInpaint/CoreInpaint.py | 7 +- src/modules/CoreInpaint/Libs/patchmatch.py | 19 +- .../CorePhotographLoaderCLI.py | 4 +- .../CustomResampleScalarVolume.py | 4 +- .../CustomizedCropVolume.py | 25 +- src/modules/CustomizedData/CustomizedData.py | 17 +- .../CustomizedDataLib/ScalarVolume.py | 54 +- .../CustomizedDataLib/Segmentation.py | 18 + .../CustomizedDataLib/VectorVolume.py | 4 +- .../CustomizedGradientAnisotropicDiffusion.py | 6 + .../Threshold/SegmentEditorThresholdEffect.py | 19 + .../CustomizedSegmentEditor.py | 3 + .../SegmentEditorSmoothingEffect.py | 24 +- src/modules/DLISImport/DLISImport.py | 7 +- .../DLISImportLib/DLISImportLogic.py | 228 +- .../DLISImportLib/DLISImportWidget.py | 68 +- .../DLISImportLib/DLISTableViewer.py | 86 +- .../SegmentEditorEffect.py | 9 + .../SegmentEditorEffect.py | 7 + src/modules/Export/Export.py | 6 +- src/modules/GeologEnv/GeologEnv.py | 85 + .../GeologLib/GeologConnectWidget.py | 265 ++ .../GeologEnv/GeologLib/GeologExportWidget.py | 427 ++ .../GeologEnv/GeologLib/GeologImportWidget.py | 356 ++ src/modules/GeologEnv/GeologLib/__init__.py | 3 + .../GeologEnv/GeologLib/scriptConnect.py | 122 + .../GeologEnv/GeologLib/scriptExport.py | 196 + .../GeologEnv/GeologLib/scriptImport.py | 78 + src/modules/GeologEnv/README.md | 30 + .../GeologEnv/Resources/Icons/GeologEnv.svg | 51 + .../HistogramSegmenter/HistogramSegmenter.py | 5 +- src/modules/ImageLogData/ImageLogData.py | 76 +- .../ImageLogDataLib/view/image_log_view.py | 46 +- src/modules/ImageLogEnv/ImageLogEnv.py | 68 +- .../KdsOptimizationTableWidget.py | 293 ++ .../ImageLogsLib/PermeabilityModeling.py | 119 +- .../ImageLogEnv/ImageLogsLib}/__init__.py | 0 .../PermeabilityModelingCLI.py | 103 +- .../PermeabilityModelingCLI.xml | 21 + .../auxiliar_functions.py | 132 +- .../ImageLogExportLib/ImageLogCSV.py | 6 +- .../widgets/ImageLogExportOpenSourceWidget.py | 162 +- .../widgets/output_name_dialog.py | 41 + .../CustomizedWidget/RenameDialog.py | 51 + .../ImageLogInpaint/ImageLogInpaint.py | 558 +++ src/modules/ImageLogInpaint/README.md | 16 + .../Resources/Icons/ImageLogInpaint.png | Bin 0 -> 22783 bytes .../ImageLogInstanceSegmenter.py | 13 +- .../Models/Islands.py | 26 +- .../Models/SidewallSample.py | 23 +- .../ImageLogInstanceSegmenter/Models/Snow.py | 24 +- .../ImageLogSegmenter/ImageLogSegmenter.py | 35 +- src/modules/ImageTools/ImageTools.py | 2 +- .../ImportCoreImagesCLI.py | 16 +- .../InstanceSegmenterCLILib/model/imagelog.py | 6 +- .../InstanceSegmenterCLILib/model/model.py | 4 +- .../InstanceSegmenterEditor.py | 6 +- src/modules/JobMonitor/JobMonitor.py | 62 +- src/modules/LabelMapEditor/LabelMapEditor.py | 8 +- .../SegmentEditorEffect.py | 26 + .../MicroCTCupsAnalysis.py | 5 +- .../Libs/MicroCTLoaderBaseWidget.py | 14 +- .../MicroCTLoader/Libs/MicroCTLoaderLogic.py | 3 +- src/modules/MicroCTLoader/Libs/RawLoader.py | 8 +- .../MicroCTTransforms/MicroCTTransforms.py | 100 +- .../Libs/microtom/microtom/porosimetry.py | 450 +- .../Libs/microtom/microtom/porosimetry_opt.py | 12 +- .../Libs/microtom/requirements.txt | 2 +- .../MicrotomRemote/Libs/microtom/setup.py | 16 +- src/modules/MicrotomRemote/MicrotomRemote.py | 1112 ++++- .../PorosimetryCLI/PorosimetryCLI.py | 37 +- .../RemoteTasks/OneResultSlurm.py | 56 +- .../ModuleInstaller/ModuleInstaller.py | 3 - .../MonaiLabelServer/MonaiLabelServer.py | 18 +- src/modules/MultiScale/MultiScale.py | 4 +- .../MultiScale/MultiscaleCLI/MultiscaleCLI.py | 22 +- .../SegmentEditorEffect.py | 41 + .../Resources/multicore_summary_template.html | 9 +- .../histogram_in_depth_analysis.py | 7 +- .../MultipleImageAnalysis.py | 3 - .../PDFTableLoaderCLI/PDFTableLoaderCLI.py | 8 +- .../PetroPUCImageLogSegmenterCLI.py | 8 +- .../PolynomialShadingCorrection.py | 4 - .../PoreNetworkCompare/PoreNetworkCompare.py | 4 + .../PoreNetworkExtractor.py | 194 +- .../PoreNetworkExtractorCLI.py | 123 + .../PoreNetworkExtractorCLI.xml | 32 + .../PoreNetworkExtractorCLILib/utils.py | 302 ++ .../visualization_widgets/curves_plot.py | 1 - .../PoreNetworkMicp/PoreNetworkMicp.py | 48 +- .../PoreNetworkProduction.py | 39 +- .../MercurySimulationLogic.py | 447 +- .../MercurySimulationWidget.py | 71 +- .../SubscaleModelWidget.py | 30 +- .../PoreNetworkSimulation.py | 88 +- .../OnePhaseSimulationWidget.py | 15 + .../PoreNetworkSimulationLogic.py | 640 ++- .../TwoPhaseSimulationWidget.py | 75 +- .../twophase/optimizer.py | 4 - .../PoreNetworkSimulationLib/widgets.py | 6 +- .../PoreNetworkSimulationCLI.py | 355 +- .../pnflow/pnflow_parallel.py | 6 +- .../pnflow/pnflow_subprocess.py | 60 +- .../subres_models.py | 262 ++ .../PoreNetworkSimulationCLILib/utils.py | 327 ++ .../PoreNetworkSimulationCLILib/vtk_utils.py | 301 ++ .../PoreNetworkVisualization.py | 5 + src/modules/QEMSCANCLI/QEMSCANCLI.py | 42 +- src/modules/RemoteService/RemoteService.py | 13 +- .../SegmentEditorEffect.py | 15 + .../SegmentInspector/SegmentInspector.py | 55 +- .../DeepWatershedLib/inference.py | 25 +- .../SegmentInspectorCLI.py | 24 +- .../SegmentationEnv/MicroCTSegmentationEnv.py | 4 +- .../SegmentationEnv/SegmentationEnv.py | 13 +- .../ThinSectionSegmentationEnv.py | 5 +- .../Methods/microporosity.py | 4 +- .../Methods/permeability.py | 7 +- .../Methods/saturated_porosity.py | 5 +- .../PermeabilityCLI/PermeabilityCLI.py | 59 +- .../SegmentationModelling.py | 20 +- .../BayesianInferenceCLI.py | 64 +- .../Segmenter/ImageLogSmartSegmenter.py | 4 +- .../MonaiModelsCLI/MonaiModelsCLI.py | 99 +- .../MonaiModelsCLILib/transforms.py | 7 +- src/modules/Segmenter/Segmenter.py | 323 +- .../SegmenterCLI/Minkowsky/minkowsky.py | 24 +- .../Segmenter/SegmenterCLI/SegmenterCLI.py | 89 +- .../ShadingCorrectionCLI.py | 4 +- src/modules/SpiralFilter/SpiralFilter.py | 1 + .../StreamlinedSegmentation.py | 16 +- .../SurfaceLoader3D/SurfaceLoader3D.py | 3 - .../ThinSectionAutoRegistration.py | 2 +- src/modules/ThinSectionEnv/ThinSectionEnv.py | 15 +- .../ThinSectionInstanceSegmenter.py | 10 +- .../ThinSectionInstanceSegmenterCLI.py | 25 +- .../ThinSectionLoader/ThinSectionLoader.py | 2 +- .../ThinSectionRegistration.py | 1 + .../ThinSectionSegmentInspector/README.md | 1 + .../Icons/ThinSectionSegmentInspector.png | Bin 0 -> 22783 bytes .../ThinSectionSegmentInspector.py | 18 + .../UnwrapSinusoidsCLI/UnwrapSinusoidsCLI.py | 20 +- .../Resources/variogram_report_template.html | 1 + .../VariogramAnalysis/VariogramAnalysis.py | 4 +- .../VolumeCalculator/VolumeCalculator.py | 8 - .../Resources/icon_geolog.svg | 51 + .../WelcomeGeoSlicer/WelcomeGeoSlicer.py | 252 +- src/modules/parse_globals.py | 38 + src/submodules/pnflow | 2 +- src/submodules/porespy | 2 +- src/submodules/pyedt | 1 - tools/bisect_bug_inspector.py | 162 + tools/commons.py | 104 - tools/delete_merged_branches.sh | 30 + tools/deploy/Customizer.py | 36 +- .../Image Log/Usabilidade/NMR_1_annotated.png | Bin 0 -> 253353 bytes .../docs/Image Log/Usabilidade/curve-1D.png | Bin 0 -> 131039 bytes .../Image Log/Usabilidade/curve-2D-BHI.png | Bin 0 -> 358446 bytes .../Usabilidade/curve-2D-labelmap.png | Bin 0 -> 65593 bytes .../docs/Image Log/Usabilidade/usabilidade.md | 50 +- .../docs/Segmenter/Semiauto/semiauto.md | 8 +- tools/deploy/GeoSlicerManual/mkdocs.yml | 6 +- tools/deploy/deploy_slicer.py | 151 +- tools/deploy/install_slicer_extensions.py | 99 - tools/deploy/requirements.txt | 1 - tools/deploy/slicer_deploy_config.json | 34 +- tools/docker/linux.Dockerfile | 2 + tools/docker/linux_base.Dockerfile | 8 +- tools/hooks/pre-commit | 54 + tools/install_pre_commit_hook.py | 48 + tools/jenkins/develop.Jenkinsfile | 76 + tools/jenkins/release.Jenkinsfile | 182 + tools/jenkins/testing.Jenkinsfile | 363 ++ tools/jenkins/updateDockerImage.Jenkinsfile | 131 + tools/jenkins/util.groovy | 242 ++ tools/new_module.py | 13 +- tools/pipeline/check_branch_root.sh | 20 + tools/pipeline/check_dependencies_licenses.sh | 30 + tools/pipeline/check_line_endings.sh | 5 + tools/pipeline/check_run_unit_tests.py | 94 + tools/pipeline/download_geoslicer_base.py | 184 + tools/pipeline/dummy_script.py | 5 + tools/pipeline/generate_test_deploy.py | 657 +++ tools/pipeline/requirements.txt | 8 + tools/pipeline/run_modules_tests.py | 143 + tools/pipeline/upload_file_bucket.py | 70 + tools/pipeline/util.py | 77 + 265 files changed, 14477 insertions(+), 8023 deletions(-) rename src/ltrace/ltrace/assets/{trained_models => public}/CoreEnv/.gitkeep (100%) rename src/ltrace/ltrace/assets/{trained_models => public}/ImageLogEnv/.gitkeep (100%) rename src/ltrace/ltrace/assets/{trained_models/ImageLogEnv/synthetic_sidewall_sample_mask_rcnn.h5 => public/ImageLogEnv/synth_side.h5} (100%) rename src/ltrace/ltrace/{slicer/tracking/__init__.py => assets/public/MicroCTEnv/.gitkeep} (100%) rename src/ltrace/ltrace/{slicer/tracking/trackers/__init__.py => assets/public/ThinSectionEnv/.gitkeep} (100%) rename src/ltrace/ltrace/assets/{trained_models => public}/unet-binary-segop.h5 (100%) delete mode 100644 src/ltrace/ltrace/assets/trained_models/ca-bundle.crt create mode 100644 src/ltrace/ltrace/image/las.py create mode 100644 src/ltrace/ltrace/image/lasGeologFile.py create mode 100644 src/ltrace/ltrace/slicer/custom_export_to_file.py delete mode 100644 src/ltrace/ltrace/slicer/equations/schema.py delete mode 100644 src/ltrace/ltrace/slicer/tracking/tracker.py delete mode 100644 src/ltrace/ltrace/slicer/tracking/trackers/module_tracker.py delete mode 100644 src/ltrace/ltrace/slicer/tracking/trackers/volume_node_tracker.py delete mode 100644 src/ltrace/ltrace/slicer/tracking/trackers/widget_tracker.py delete mode 100644 src/ltrace/ltrace/slicer/tracking/tracking_manager.py create mode 100644 src/ltrace/ltrace/utils/blob2hash.py create mode 100644 src/ltrace/ltrace/workflow/workstep/data/ExportLAS.py create mode 100644 src/ltrace/ltrace/workflow/workstep/segmentation/InspectorIslands.py create mode 100644 src/ltrace/ltrace/workflow/workstep/segmentation/ThinSectionPores.py create mode 100644 src/ltrace/ltrace/workflow/workstep/simulation/InputTablesListWidget.py create mode 100644 src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimMercury.py create mode 100644 src/modules/GeologEnv/GeologEnv.py create mode 100644 src/modules/GeologEnv/GeologLib/GeologConnectWidget.py create mode 100644 src/modules/GeologEnv/GeologLib/GeologExportWidget.py create mode 100644 src/modules/GeologEnv/GeologLib/GeologImportWidget.py create mode 100644 src/modules/GeologEnv/GeologLib/__init__.py create mode 100644 src/modules/GeologEnv/GeologLib/scriptConnect.py create mode 100644 src/modules/GeologEnv/GeologLib/scriptExport.py create mode 100644 src/modules/GeologEnv/GeologLib/scriptImport.py create mode 100644 src/modules/GeologEnv/README.md create mode 100644 src/modules/GeologEnv/Resources/Icons/GeologEnv.svg create mode 100644 src/modules/ImageLogEnv/ImageLogsLib/KdsOptimizationTableWidget.py rename {tools => src/modules/ImageLogEnv/ImageLogsLib}/__init__.py (100%) create mode 100644 src/modules/ImageLogExport/ImageLogExportLib/widgets/output_name_dialog.py create mode 100644 src/modules/ImageLogInpaint/CustomizedWidget/RenameDialog.py create mode 100644 src/modules/ImageLogInpaint/ImageLogInpaint.py create mode 100644 src/modules/ImageLogInpaint/README.md create mode 100644 src/modules/ImageLogInpaint/Resources/Icons/ImageLogInpaint.png create mode 100644 src/modules/PoreNetworkExtractorCLI/PoreNetworkExtractorCLI.py create mode 100644 src/modules/PoreNetworkExtractorCLI/PoreNetworkExtractorCLI.xml create mode 100644 src/modules/PoreNetworkExtractorCLI/PoreNetworkExtractorCLILib/utils.py create mode 100644 src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/subres_models.py create mode 100644 src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/utils.py create mode 100644 src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/vtk_utils.py create mode 100644 src/modules/ThinSectionSegmentInspector/README.md create mode 100644 src/modules/ThinSectionSegmentInspector/Resources/Icons/ThinSectionSegmentInspector.png create mode 100644 src/modules/ThinSectionSegmentInspector/ThinSectionSegmentInspector.py create mode 100644 src/modules/WelcomeGeoSlicer/Resources/icon_geolog.svg create mode 100644 src/modules/parse_globals.py delete mode 160000 src/submodules/pyedt create mode 100644 tools/bisect_bug_inspector.py delete mode 100644 tools/commons.py create mode 100644 tools/delete_merged_branches.sh create mode 100644 tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/NMR_1_annotated.png create mode 100644 tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-1D.png create mode 100644 tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-BHI.png create mode 100644 tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-labelmap.png delete mode 100644 tools/deploy/install_slicer_extensions.py create mode 100644 tools/hooks/pre-commit create mode 100644 tools/install_pre_commit_hook.py create mode 100644 tools/jenkins/develop.Jenkinsfile create mode 100644 tools/jenkins/release.Jenkinsfile create mode 100644 tools/jenkins/testing.Jenkinsfile create mode 100644 tools/jenkins/updateDockerImage.Jenkinsfile create mode 100644 tools/jenkins/util.groovy create mode 100644 tools/pipeline/check_branch_root.sh create mode 100644 tools/pipeline/check_dependencies_licenses.sh create mode 100644 tools/pipeline/check_line_endings.sh create mode 100644 tools/pipeline/check_run_unit_tests.py create mode 100644 tools/pipeline/download_geoslicer_base.py create mode 100644 tools/pipeline/dummy_script.py create mode 100644 tools/pipeline/generate_test_deploy.py create mode 100644 tools/pipeline/requirements.txt create mode 100644 tools/pipeline/run_modules_tests.py create mode 100644 tools/pipeline/upload_file_bucket.py create mode 100644 tools/pipeline/util.py diff --git a/.gitmodules b/.gitmodules index 5b37604..6ce9408 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "src/submodules/pyedt"] - path = src/submodules/pyedt - url = git@github.com:ltracegeo/pyedt.git [submodule "src/submodules/porespy"] path = src/submodules/porespy url = git@github.com:ltracegeo/porespy.git diff --git a/pyproject.toml b/pyproject.toml index 549c628..54f3853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,17 +55,26 @@ authorized_licenses = [ "BSD-3-clause", "BSD 3-Clause", "BSD License", + "CC-BY", "GNU Library or Lesser General Public License (LGPL)", + "GNU Lesser General Public License v2 or later (LGPLv2+)", + "ISC License (ISCL)", "LGPL", "LGPL-3.0", "MIT", + "MIT License", "Mozilla Public License 2.0 (MPL 2.0)", "new BSD", "OSI Approved", "Python Software Foundation", "Historical Permission Notice and Disclaimer (HPND)", - "Other/Proprietary" ] unauthorized_licenses = [ "GNU General Public License v3 (GPLv3)" ] +[tool.liccheck.authorized_packages] +# The packages below are listed as "Other/Proprietary" +# They use Intel licenses that allow distribution +mkl = ">=2022.2.1" +tbb = ">=2021.12.0" +intel-openmp = ">=2022.2.1" diff --git a/src/ltrace/ltrace/algorithms/detect_cups.py b/src/ltrace/ltrace/algorithms/detect_cups.py index c8c7076..f53c795 100644 --- a/src/ltrace/ltrace/algorithms/detect_cups.py +++ b/src/ltrace/ltrace/algorithms/detect_cups.py @@ -163,9 +163,18 @@ def detect_rock_cylinder(array): PADDING = 15 +def find_suitable_null_value(array): + if np.issubdtype(array.dtype, np.floating): + return int(array.min()) - 1 + if np.issubdtype(array.dtype, np.signedinteger): + return array.min() - 1 + if np.issubdtype(array.dtype, np.unsignedinteger): + return 0 + + def crop_cylinder(array, cylinder): x, y, r, z_min, z_max = cylinder - min_ = array.min() + null_value = np.array(find_suitable_null_value(array)).astype(array.dtype) x_min, x_max = round(x - r), round(x + r) y_min, y_max = round(y - r), round(y + r) z_min, z_max = round(z_min), round(z_max) @@ -178,10 +187,10 @@ def crop_cylinder(array, cylinder): dist = np.sqrt((xx - r) ** 2 + (yy - r) ** 2) mask = dist <= r mask = mask[np.newaxis, ...] - array = (array * mask) + (~mask * min_) + array = (array * mask) + (~mask * null_value) - array = np.pad(array, ((0, 0), (PADDING, PADDING), (PADDING, PADDING)), mode="constant", constant_values=min_) - return array + array = np.pad(array, ((0, 0), (PADDING, PADDING), (PADDING, PADDING)), mode="constant", constant_values=null_value) + return array, null_value def get_origin_offset(cylinder): @@ -314,10 +323,10 @@ def full_detect(array, callback=lambda *args: None): logging.debug(f"x, y, r, z0, z1 = {cylinder}") callback(50, "Cropping cylinder") - rock = crop_cylinder(array, cylinder) + rock, null_value = crop_cylinder(array, cylinder) callback(70, "Detecting cups") cup_left, cup_right = isolated_cups_slice(array, cylinder) refs = detect_cups_from_sides(cup_left, cup_right) if refs: logging.debug(f"(Q - T) / (A - T) = {quartz_ratio(refs)}") - return rock, refs, cylinder + return rock, null_value, refs, cylinder diff --git a/src/ltrace/ltrace/algorithms/supervised/mrcnn/utils.py b/src/ltrace/ltrace/algorithms/supervised/mrcnn/utils.py index aedcde2..2fc894a 100644 --- a/src/ltrace/ltrace/algorithms/supervised/mrcnn/utils.py +++ b/src/ltrace/ltrace/algorithms/supervised/mrcnn/utils.py @@ -849,11 +849,8 @@ def download_trained_weights(coco_model_path, verbose=1): """ if verbose > 0: print("Downloading pretrained model to " + coco_model_path + " ...") - with open(coco_model_path, "wb") as out: - resp = urllib.request.urlopen(COCO_MODEL_URL) + with urllib.request.urlopen(COCO_MODEL_URL) as resp, open(coco_model_path, "wb") as out: shutil.copyfileobj(resp, out) - resp.close() - if verbose > 0: print("... done downloading pretrained model!") diff --git a/src/ltrace/ltrace/assets/trained_models/CoreEnv/.gitkeep b/src/ltrace/ltrace/assets/public/CoreEnv/.gitkeep similarity index 100% rename from src/ltrace/ltrace/assets/trained_models/CoreEnv/.gitkeep rename to src/ltrace/ltrace/assets/public/CoreEnv/.gitkeep diff --git a/src/ltrace/ltrace/assets/trained_models/ImageLogEnv/.gitkeep b/src/ltrace/ltrace/assets/public/ImageLogEnv/.gitkeep similarity index 100% rename from src/ltrace/ltrace/assets/trained_models/ImageLogEnv/.gitkeep rename to src/ltrace/ltrace/assets/public/ImageLogEnv/.gitkeep diff --git a/src/ltrace/ltrace/assets/trained_models/ImageLogEnv/synthetic_sidewall_sample_mask_rcnn.h5 b/src/ltrace/ltrace/assets/public/ImageLogEnv/synth_side.h5 similarity index 100% rename from src/ltrace/ltrace/assets/trained_models/ImageLogEnv/synthetic_sidewall_sample_mask_rcnn.h5 rename to src/ltrace/ltrace/assets/public/ImageLogEnv/synth_side.h5 diff --git a/src/ltrace/ltrace/slicer/tracking/__init__.py b/src/ltrace/ltrace/assets/public/MicroCTEnv/.gitkeep similarity index 100% rename from src/ltrace/ltrace/slicer/tracking/__init__.py rename to src/ltrace/ltrace/assets/public/MicroCTEnv/.gitkeep diff --git a/src/ltrace/ltrace/slicer/tracking/trackers/__init__.py b/src/ltrace/ltrace/assets/public/ThinSectionEnv/.gitkeep similarity index 100% rename from src/ltrace/ltrace/slicer/tracking/trackers/__init__.py rename to src/ltrace/ltrace/assets/public/ThinSectionEnv/.gitkeep diff --git a/src/ltrace/ltrace/assets/trained_models/unet-binary-segop.h5 b/src/ltrace/ltrace/assets/public/unet-binary-segop.h5 similarity index 100% rename from src/ltrace/ltrace/assets/trained_models/unet-binary-segop.h5 rename to src/ltrace/ltrace/assets/public/unet-binary-segop.h5 diff --git a/src/ltrace/ltrace/assets/trained_models/ca-bundle.crt b/src/ltrace/ltrace/assets/trained_models/ca-bundle.crt deleted file mode 100644 index 32ad65e..0000000 --- a/src/ltrace/ltrace/assets/trained_models/ca-bundle.crt +++ /dev/null @@ -1,3772 +0,0 @@ -# Petrobras AC Emissora Corporativa ------BEGIN CERTIFICATE----- -MIIFXDCCA0SgAwIBAgIKa15xsgACAAAACjANBgkqhkiG9w0BAQUFADAoMSYwJAYD -VQQDEx1QZXRyb2JyYXMgQ0EgUm9vdCBDb3Jwb3JhdGl2YTAeFw0xMDA2MTUxNDA5 -MjdaFw0yMDA2MTUxNDE5MjdaMFwxEzARBgoJkiaJk/IsZAEZFgNiaXoxGTAXBgoJ -kiaJk/IsZAEZFglwZXRyb2JyYXMxKjAoBgNVBAMTIVBldHJvYnJhcyBBQyBFbWlz -c29yYSBDb3Jwb3JhdGl2YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ALiP22Bp4HQw9aJmx3t2vIRDsLExgXp7gMHU1YCvhh4Jv0UsbPn5pkcWMiaIkQla -WIAQ17Opj3SOyf3cnS2hP5WAYFnR0ahCFkh3i94uVKHOWMCvjhFHbpFxiYS7patS -wlTp8RvveWeMhhbCkNHPSzr5KnBLSAvlskbJ6XKWE5SNrUw2G/SlA/dymsAQyAIN -Hy2XRfhsGNlmW0e5F+BSKM57tB/e1bgs+xOLT+z7X57lVAoDWRG69TbHpRa8odFV -ZNnjX14M1Nfv33xo3RzIODPvgvZLa9pCKtYwNdBKJFVpAAg1A+8ylS+CaB+dzMjt -MZ4rkOaP4/sgwD6ahah5SFECAwEAAaOCAVIwggFOMBIGA1UdEwEB/wQIMAYBAf8C -AQAwHQYDVR0OBBYEFEjstyHQllQgEuQPD0knidHGo8UWMAsGA1UdDwQEAwIBhjAQ -BgkrBgEEAYI3FQEEAwIBADAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAfBgNV -HSMEGDAWgBQ2XmGG391UfPeE235pD983euOfoDBXBgNVHR8EUDBOMEygSqBIhkZo -dHRwOi8vcGtpLnBldHJvYnJhcy5jb20uYnIvUGV0cm9icmFzJTIwQ0ElMjBSb290 -JTIwQ29ycG9yYXRpdmEoMikuY3JsMGUGCCsGAQUFBwEBBFkwVzBVBggrBgEFBQcw -AoZJaHR0cDovL3BraS5wZXRyb2JyYXMuY29tLmJyL1BldHJvYnJhcyUyMENBJTIw -Um9vdCUyMENvcnBvcmF0aXZhKDIpKDIpLmNydDANBgkqhkiG9w0BAQUFAAOCAgEA -pY5V9Lzwe90NdKENLl3LnQS/dOhMBBhHdH+cM/TOQde0mnIVOKd8qpqDYp81KJkq -ZSxiROGiDb3jNRDnaPPB9CyGcDtzsp7vQBbqVU0BwaiXMTMMtPE4DVEgj89shbrl -Hf1gBrlIYLYar+yQQ8m3wxqut0xokCqpJCHvqGDTKRWjMIhZqwuJqNSFYN+K4egs -SOBV0pb4HvEdlXjcQsMyfqi1vJ7sjhioUGn8cCxnssoXSUtdBNPQTpQ9pst+dKJQ -0DnJJ1rfcQxiC1hhXlKLjEYolLxnY1Tk6QXDL0zENXNdagm4qkiE3UdYh2NhXTj7 -1yo+YS01JOeYfUTfmTfsUZRzfvmbNeXDa8AeMmhwv1nqwtH8O2D0tb/ZAaiz0tnW -H8e6VzRL6kIwmmRb/SxtxXuhGm4M15vciWBg/+pCxqT/iZl7kCcMla0GWLMCfj9r -nDxTXqB5P9Ciqj2LB76yWbk2ebXtDVJ3kn43HqqcScajhzyYxGUnrg5GLVVs5AAF -+OLdElLYpGdglQ/uO/3ZPLWZ8oKm+MZQzaE2YIHOmG5817EDjT2kvlW/5haz6WW2 -g+aoIbEIPyZO036QKGFe/ksI6V5sHZF2FKq9nyUY5cx1Z0glW3G+485hzATOidgp -OekagwhU6J1Hky5Bs+dnDY6FW/QLgtY6cC/59freyNI= ------END CERTIFICATE----- - -# Petrobras CA Emissora Corporativa ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIKGOf1oAACAAAADTANBgkqhkiG9w0BAQUFADAoMSYwJAYD -VQQDEx1QZXRyb2JyYXMgQ0EgUm9vdCBDb3Jwb3JhdGl2YTAeFw0xNzEwMTcxODM2 -MjJaFw0yNzEwMTcxODQ2MjJaMFwxEzARBgoJkiaJk/IsZAEZFgNiaXoxGTAXBgoJ -kiaJk/IsZAEZFglwZXRyb2JyYXMxKjAoBgNVBAMTIVBldHJvYnJhcyBDQSBFbWlz -c29yYSBDb3Jwb3JhdGl2YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ALsNEDoZ/Hhm0bp9z803tmY6gBW20YV4Ss6KNQpKq0IR4ig1k8d4PcZjFTWmAMlM -Ofls3lwfgSqHf1CobxYfwWECYEqjjHsSPTs3CnjeXGbBceSruLN6kNwO2p6WnaM5 -/LfJMZrOh2FsnMDGrvjnUE2AWLvV5f8/VSK/q9Ese31eZsEa1qzW0MIcAx6kJ+f2 -d13pQ1ugLHPaU7FZxiReZfgQJgkrmbH4nv5i5Ws+Y62CQdJnUWG/s0fb1tMDcdmP -b9da+sxWDVI5qNAJ/pOOJ63ONSRtIfL/Ig6lKYotUsNPWKsGRZ627fc3Z9eXnh35 -+Umb6vzABmAe5e4LnXon3mMCAwEAAaOCAU8wggFLMBAGCSsGAQQBgjcVAQQDAgEA -MB0GA1UdDgQWBBQDKd1ML8TwPZAYUaEDl30vzXQkTzAZBgkrBgEEAYI3FAIEDB4K -AFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME -GDAWgBQ2XmGG391UfPeE235pD983euOfoDBXBgNVHR8EUDBOMEygSqBIhkZodHRw -Oi8vcGtpLnBldHJvYnJhcy5jb20uYnIvUGV0cm9icmFzJTIwQ0ElMjBSb290JTIw -Q29ycG9yYXRpdmEoMikuY3JsMGUGCCsGAQUFBwEBBFkwVzBVBggrBgEFBQcwAoZJ -aHR0cDovL3BraS5wZXRyb2JyYXMuY29tLmJyL1BldHJvYnJhcyUyMENBJTIwUm9v -dCUyMENvcnBvcmF0aXZhKDIpKDIpLmNydDANBgkqhkiG9w0BAQUFAAOCAgEASMSv -WUMVY+aM7jB0e3MokGZfWy6w0yGm9JiRJ/HXemKmdlW129CRYaSrLRqsmUrXN0MC -tsYGVqGrEFYVA6Bob6A14X7T5D4259ixItqLOPougGF7slgzoLQOJ6t7HvRCLt/Y -wWwEWGJamzY3jR3ROxhpmie5Ar9+y9fMO1kPbmnMQFeuwyNUCVrxBQNS0KdmtxJS -TtR+tpXF92hq2cvjqxY1ixB5+5yXgPJfqsYdxfR+VKeni7uEf7cMN4TjULkRyTi7 -9+Eagemx82adzrjHjfXybuK7gvsXuozPd+HW8ggIRfs/J0AtxE4cO7C7/oscO1sY -uvzz8hosYAOblN+w0kVGlFLTB9YUbIm9+MUjPgvOrC1G5+YgzrHHOqr7si/1MXvU -7Ch+Vm0EvgqMNsl0bpA1sBOc6PBhdTc4d/xIMmVwzRjAVNMFCJDyGVAd5nIhlL3R -K/u7Z+Xi0ejjSTS232RYVYAilJu6uPfWBhHDq45+sdZNVxa9ZetALxNUbcM8YM+C -D1ep7GyxYQ5yoNQxNfMjzyVwA70yyRL044q/x8/OU/KIpf5sv+MVkzoHKTp7kR71 -xc7gdAH3ktxxp24ZvyAyN4JcSrWLnwvVmwPdBsvrhXzUCBVytdwB+dI20nI8+C6J -xwD7PXD8SGV0zZ9Y2OHT1X7sVM9o7AzRplyxvxY= ------END CERTIFICATE----- - -# Petrobras CA Root Corporativa ------BEGIN CERTIFICATE----- -MIIFUjCCAzqgAwIBAgIQLQugNHY8qohHytvD5vz5MjANBgkqhkiG9w0BAQUFADAo -MSYwJAYDVQQDEx1QZXRyb2JyYXMgQ0EgUm9vdCBDb3Jwb3JhdGl2YTAeFw0xMDA0 -MjcxMzQxMTVaFw0zMDA0MjcxMzM0MjdaMCgxJjAkBgNVBAMTHVBldHJvYnJhcyBD -QSBSb290IENvcnBvcmF0aXZhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -AgEA0q+Si4hzhKKyGqRlEVu9hbSRXagn1eq0huFeb7GpnBdh561fTAxjOKHZ4lUA -gTrt5CtKODz68UJKFZR7U1Fs/yOQPZqqcGknsCC6RW2x+eBB5ebAJ3S/ane67hWJ -uC7SdF+g2wfQ+Q7SzCdQ+w6h0NovaYdO9AqMciOYBejB7UPq0VuJUAfVq+s7j1WB -3zYw2COCIZVZBY5GDrM1Z0PAJ4+XcPEI0jVIFE9FDP4N0ttr8izW8z4kUP/A63QI -YJGRxkQ6D6LxvwADcwAWezeSoh+OvZ6hlA4cgnzWa7QNEplZxqHqWfe9AUwEgyyW -XpkxjMagMM+a2UvoQnWIenN0Md+nMuJ7/COuZqh0D1G1aGB5txBe1iYp/uHweMDA -uZancj07W6jsn/j8XLvlA007avZDOOUJ9kwUpNH9VErpqLu/zY2fwbJiPVA/7hlx -3IsvSOwVQzjTJOhQM+BchzxbB6q6YDoNmlLqYuzqZQLSeHZratw5UR8sorEzejyc -5A3QC/TdYh956RCU791uhhhsOOHUhizOT3bV5/bM+kFgM2RQ4WJUDHmooxm6ipHp -hHS2o0MC+UEp5LX6+9+dyWxI6AipmTK98ufTrJigCtLq4W13dTTLK5ehWk48/qHT -/+D6Yp2xYrOQosAyYO5uMqVBufHjpq59hQuePDqh+sTxGycCAwEAAaN4MHYwCwYD -VR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDZeYYbf3VR894Tb -fmkP3zd645+gMCMGCSsGAQQBgjcVAgQWBBR23KzX/U/7rksPyE2t7Sa0Iy2aTjAS -BgkrBgEEAYI3FQEEBQIDAgACMA0GCSqGSIb3DQEBBQUAA4ICAQC0SI/tUzq77zxV -Z9yE5haOZeHa8YGyPyiu7lcmB+8OBTLcK2EyOaFPguKRDlUbvNcnBwGa2TNtyJWH -VNDX85OWWb+4xho9AAnvmLTMVKuHynOemBhoLY7G6R/JhQRev9zjWPfi6ecjVHre -z/XcM4aV4xqyF4VDlOO8/BPPgtMoABe6VqEAzuxu1CKWHCBiM/WFhsiRO3eWo3I3 -Pcz9Rgpr/OZH7HxiA52lQy0e3sQiC01G5Sa64qLH/nhBGjZi8qU6sNNGhKYM4qoe -lKwSQnYr70bWyo8Uo69GJJ7LOizXkfnYb4GYBObX1U8GFQSGleWUbkvw8kl8u9tW -MOElaPOf+9fv0NAprLj8V9KIt4G+qPV2zuN+pWJXCwQ3ka48gv7LxwGaqxoN1Ta1 -tYkC6cizACXfZSSx/6AHgFaDJ9khno34MMTuTKLkgIj5UVZq8jdIByvDP4qaI9IY -RDLTzPrIOpCVl1oskNnBifq1UekCUNqRhhP4nbYGxcO1GPKSS/HBsTby+gMBAN+J -Welx9RcpL4UxHdIP/qJQw8Vh89MKvAQWpmD6EnkEPu0o8GZ9o1JrxpYuZehVAeHZ -PuytuOsT3lEjxcwaMSyLm0Gmy7lcwL+ocTLRXZvEfjmuBrZVM35iLz6me0rKlTkj -8yw5Pr4I1GeoiTx2KXck2xk35ba6sQ== ------END CERTIFICATE----- - -# ACCVRAIZ1 ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE -AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw -CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ -BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND -VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb -qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY -HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo -G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA -lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr -IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ -0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH -k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 -4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO -m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa -cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl -uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI -KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls -ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG -AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT -VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG -CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA -cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA -QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA -7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA -cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA -QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA -czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu -aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt -aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud -DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF -BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp -D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU -JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m -AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD -vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms -tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH -7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA -h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF -d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H -pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -# AC RAIZ FNMT-RCM ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx -CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ -WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ -BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG -Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ -yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf -BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz -WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF -tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z -374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC -IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL -mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 -wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS -MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 -ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet -UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H -YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 -LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 -RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM -LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf -77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N -JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm -fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp -6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp -1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B -9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok -RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv -uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -# Actalis Authentication Root CA ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE -BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w -MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC -SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 -ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv -UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX -4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 -KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ -gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb -rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ -51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F -be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe -KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F -v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn -fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 -jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz -ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL -e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 -jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz -WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V -SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j -pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX -X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok -fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R -K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU -ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU -LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT -LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -# AddTrust External Root ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs -IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 -MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h -bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt -H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 -uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX -mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX -a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN -E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 -WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD -VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 -Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU -cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx -IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN -AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH -YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC -Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX -c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a -mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- - -# AffirmTrust Commercial ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz -dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL -MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp -cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP -Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr -ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL -MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 -yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr -VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ -nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ -KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG -XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj -vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt -Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g -N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC -nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE----- - -# AffirmTrust Networking ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz -dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL -MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp -cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y -YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua -kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL -QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp -6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG -yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i -QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ -KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO -tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu -QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ -Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u -olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 -x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= ------END CERTIFICATE----- - -# AffirmTrust Premium ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz -dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG -A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U -cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf -qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ -JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ -+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS -s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 -HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 -70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG -V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S -qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S -5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia -C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX -OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE -FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 -KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg -Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B -8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ -MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc -0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ -u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF -u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH -YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 -GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO -RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e -KeC2uAloGRwYQw== ------END CERTIFICATE----- - -# AffirmTrust Premium ECC ------BEGIN CERTIFICATE----- -MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC -VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ -cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ -BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt -VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D -0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 -ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G -A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs -aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I -flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== ------END CERTIFICATE----- - -# Amazon Root CA 1 ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -# Amazon Root CA 2 ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE----- - -# Amazon Root CA 3 ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- - -# Amazon Root CA 4 ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -# Atos TrustedRoot 2011 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -# Autoridad de Certificacion Firmaprofesional CIF A62634068 ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE -BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h -cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy -MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg -Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 -thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM -cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG -L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i -NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h -X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b -m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy -Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja -EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T -KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF -6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh -OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD -VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD -VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp -cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv -ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl -AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF -661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 -am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 -ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 -PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS -3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k -SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF -3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM -ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g -StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz -Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB -jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V ------END CERTIFICATE----- - -# Baltimore CyberTrust Root ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ -RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD -VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX -DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y -ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy -VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr -mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr -IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK -mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu -XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy -dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye -jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 -BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 -DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 -9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx -jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 -Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz -ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS -R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE----- - -# Buypass Class 2 Root CA ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- - -# Buypass Class 3 Root CA ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y -ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E -N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 -tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX -0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c -/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X -KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY -zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS -O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D -34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP -K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv -Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj -QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS -IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 -HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa -O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv -033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u -dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE -kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 -3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD -u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq -4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE----- - -# CA Disig Root R2 ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy -MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe -NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH -PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I -x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe -QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR -yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO -QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 -H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ -QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD -i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs -nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 -rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI -hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf -GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb -lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka -+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal -TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i -nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 -gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr -G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os -zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x -L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -# CFCA EV ROOT ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -# COMODO Certification Authority ------BEGIN CERTIFICATE----- -MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB -gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV -BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw -MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl -YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P -RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 -UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI -2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 -Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp -+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ -DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O -nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW -/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g -PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u -QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY -SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv -IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ -RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 -zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd -BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB -ZQ== ------END CERTIFICATE----- - -# COMODO ECC Certification Authority ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -# COMODO RSA Certification Authority ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB -hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV -BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT -EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR -6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X -pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC -9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV -/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf -Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z -+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w -qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah -SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC -u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf -Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq -crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl -wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM -4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV -2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna -FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ -CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK -boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke -jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL -S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb -QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl -0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB -NVOFBkpdn627G190 ------END CERTIFICATE----- - -# Certigna ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV -BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X -DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ -BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 -QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny -gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw -zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q -130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 -JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw -ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT -AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj -AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG -9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h -bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc -fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu -HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w -t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE----- - -# Certinomis - Root CA ------BEGIN CERTIFICATE----- -MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET -MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb -BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz -MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx -FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g -Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 -fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl -LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV -WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF -TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb -5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc -CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri -wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ -wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG -m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 -F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng -WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB -BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 -2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF -AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ -0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw -F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS -g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj -qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN -h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ -ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V -btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj -Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ -8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW -gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= ------END CERTIFICATE----- - -# Certplus Class 2 Primary CA ------BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw -PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz -cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 -MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz -IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ -ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR -VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL -kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd -EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas -H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 -HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud -DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 -QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu -Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ -AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 -yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR -FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA -ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB -kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 -l7+ijrRU ------END CERTIFICATE----- - -# Certplus Root CA G1 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA -MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy -dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa -MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy -dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a -iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt -6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP -0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f -6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE -EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN -1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc -h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT -mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV -4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO -WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud -DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd -Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq -hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh -66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7 -/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS -S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j -2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R -Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr -RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy -6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV -V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5 -g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl -++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo= ------END CERTIFICATE----- - -# Certplus Root CA G2 ------BEGIN CERTIFICATE----- -MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x -CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs -dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x -CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs -dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat -93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x -Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P -AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj -FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG -SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch -p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal -U5ORGpOucGpnutee5WEaXw== ------END CERTIFICATE----- - -# Certum Trusted Network CA ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM -MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D -ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU -cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 -WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg -Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw -IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH -UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM -TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU -BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM -kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x -AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV -HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y -sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL -I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 -J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY -VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -# Certum Trusted Network CA 2 ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB -gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu -QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG -A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz -OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA -DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn -0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB -OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE -fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E -Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m -o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i -sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW -OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez -Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS -adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n -3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ -F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf -CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 -XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm -djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ -WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb -AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq -P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko -b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj -XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P -5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi -DrW5viSP ------END CERTIFICATE----- - -# Chambers of Commerce Root - 2008 ------BEGIN CERTIFICATE----- -MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD -VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 -IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 -MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz -IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz -MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj -dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw -EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp -MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 -28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq -VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q -DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR -5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL -ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a -Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl -UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s -+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 -Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj -ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx -hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV -HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 -+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN -YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t -L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy -ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt -IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV -HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w -DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW -PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF -5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 -glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH -FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 -pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD -xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG -tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq -jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De -fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg -OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ -d0jQ ------END CERTIFICATE----- - -# Comodo AAA Services root ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj -YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM -GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua -BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe -3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 -YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR -rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm -ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU -oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v -QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t -b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF -AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q -GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 -G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi -l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 -smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- - -# Cybertrust Global Root ------BEGIN CERTIFICATE----- -MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG -A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh -bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE -ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS -b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 -7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS -J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y -HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP -t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz -FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY -XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ -MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw -hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js -MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA -A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj -Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx -XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o -omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc -A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW -WL1WMRJOEcgh4LMRkWXbtKaIOM5V ------END CERTIFICATE----- - -# D-TRUST Root Class 3 CA 2 2009 ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -# D-TRUST Root Class 3 CA 2 EV 2009 ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -# DST Root CA X3 ------BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ -MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT -DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow -PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD -Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O -rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq -OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b -xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw -7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD -aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG -SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 -ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr -AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz -R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 -JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo -Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ ------END CERTIFICATE----- - -# Deutsche Telekom Root CA 2 ------BEGIN CERTIFICATE----- -MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc -MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj -IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB -IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE -RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl -U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 -IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU -ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC -QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr -rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S -NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc -QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH -txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP -BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC -AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp -tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa -IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl -6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ -xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU -Cm26OWMohpLzGITY+9HPBVZkVw== ------END CERTIFICATE----- - -# DigiCert Assured ID Root CA ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c -JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP -mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ -wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 -VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ -AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB -AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun -pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC -dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf -fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm -NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx -H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -# DigiCert Assured ID Root G2 ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA -n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc -biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp -EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA -bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu -YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB -AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW -BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI -QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I -0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni -lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 -B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv -ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -# DigiCert Assured ID Root G3 ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- - -# DigiCert Global Root CA ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -# DigiCert Global Root G2 ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -# DigiCert Global Root G3 ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE----- - -# DigiCert High Assurance EV Root CA ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j -ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 -LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug -RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm -+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW -PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM -xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB -Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 -hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg -EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA -FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec -nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z -eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF -hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 -Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep -+OkuE6N36B9K ------END CERTIFICATE----- - -# DigiCert Trusted Root G4 ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg -RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y -ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If -xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV -ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO -DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ -jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ -CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi -EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM -fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY -uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK -chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t -9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 -SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd -+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc -fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa -sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N -cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N -0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie -4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI -r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 -/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm -gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE----- - -# E-Tugra Certification Authority ------BEGIN CERTIFICATE----- -MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV -BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC -aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV -BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 -Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz -MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ -BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp -em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN -ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY -B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH -D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF -Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo -q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D -k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH -fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut -dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM -ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 -zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn -rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX -U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 -Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 -XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF -Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR -HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY -GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c -77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 -+GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK -vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 -FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl -yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P -AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD -y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d -NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== ------END CERTIFICATE----- - -# EC-ACC ------BEGIN CERTIFICATE----- -MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB -8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy -dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 -YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 -dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh -IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD -LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG -EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g -KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD -ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu -bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg -ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R -85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm -4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV -HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd -QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t -lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB -o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 -opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo -dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW -ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN -AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y -/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k -SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy -Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS -Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl -nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= ------END CERTIFICATE----- - -# EE Certification Centre Root CA ------BEGIN CERTIFICATE----- -MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy -MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl -ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS -b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy -euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO -bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw -WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d -MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE -1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ -zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB -BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF -BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV -v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG -E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u -uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW -iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v -GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= ------END CERTIFICATE----- - -# Entrust.net Premium 2048 Secure Server CA ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML -RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp -bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 -IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 -MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 -LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp -YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG -A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq -K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe -sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX -MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT -XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ -HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH -4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub -j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo -U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf -zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b -u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ -bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er -fF6adulZkMV8gzURZVE= ------END CERTIFICATE----- - -# Entrust Root Certification Authority ------BEGIN CERTIFICATE----- -MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 -Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW -KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw -NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw -NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy -ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV -BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo -Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 -4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 -KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI -rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi -94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB -sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi -gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo -kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE -vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA -A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t -O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua -AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP -9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ -eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m -0vdXcDazv/wor3ElhVsT/h5/WrQ8 ------END CERTIFICATE----- - -# Entrust Root Certification Authority - EC1 ------BEGIN CERTIFICATE----- -MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG -A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 -d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu -dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq -RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy -MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD -VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 -L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g -Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD -ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi -A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt -ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH -Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O -BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC -R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX -hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G ------END CERTIFICATE----- - -# Entrust Root Certification Authority - G2 ------BEGIN CERTIFICATE----- -MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 -cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs -IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz -dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy -NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu -dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt -dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 -aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T -RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN -cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW -wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 -U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 -jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN -BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ -jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ -Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v -1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R -nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH -VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== ------END CERTIFICATE----- - -# GDCA TrustAUTH R5 ROOT ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE -BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 -MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV -BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w -HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj -Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj -TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u -KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj -qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm -MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 -ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP -zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk -L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC -jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA -HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC -AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm -DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 -COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry -L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf -JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg -IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io -2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV -09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ -XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq -T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe -MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -# GeoTrust Global CA ------BEGIN CERTIFICATE----- -MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT -MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i -YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG -EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg -R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 -9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq -fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv -iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU -1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ -bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW -MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA -ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l -uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn -Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS -tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF -PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un -hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV -5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== ------END CERTIFICATE----- - -# GeoTrust Primary Certification Authority ------BEGIN CERTIFICATE----- -MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY -MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo -R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx -MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK -Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 -AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA -ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 -7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W -kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI -mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ -KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 -6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl -4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K -oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj -UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU -AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= ------END CERTIFICATE----- - -# GeoTrust Primary Certification Authority - G2 ------BEGIN CERTIFICATE----- -MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL -MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj -KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 -MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 -eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV -BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw -NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV -BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH -MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL -So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal -tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG -CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT -qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz -rD6ogRLQy7rQkgu2npaqBA+K ------END CERTIFICATE----- - -# GeoTrust Primary Certification Authority - G3 ------BEGIN CERTIFICATE----- -MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB -mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT -MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ -BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg -MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 -BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz -+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm -hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn -5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W -JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL -DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC -huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw -HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB -AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB -zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN -kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD -AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH -SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G -spki4cErx5z481+oghLrGREt ------END CERTIFICATE----- - -# GeoTrust Universal CA ------BEGIN CERTIFICATE----- -MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy -c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE -BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 -IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV -VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 -cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT -QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh -F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v -c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w -mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd -VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX -teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ -f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe -Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ -nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB -/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY -MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG -9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc -aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX -IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn -ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z -uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN -Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja -QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW -koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 -ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt -DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm -bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= ------END CERTIFICATE----- - -# GeoTrust Universal CA 2 ------BEGIN CERTIFICATE----- -MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy -c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD -VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 -c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 -WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG -FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq -XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL -se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb -KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd -IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 -y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt -hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc -QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 -Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV -HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ -KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z -dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ -L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr -Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo -ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY -T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz -GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m -1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV -OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH -6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX -QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS ------END CERTIFICATE----- - -# GlobalSign ECC Root CA - R4 ------BEGIN CERTIFICATE----- -MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ -FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F -uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX -kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs -ewv4n4Q= ------END CERTIFICATE----- - -# GlobalSign ECC Root CA - R5 ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -# GlobalSign Root CA ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG -A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv -b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw -MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i -YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT -aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ -jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp -xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp -1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG -snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ -U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 -9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B -AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz -yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE -38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP -AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad -DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME -HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- - -# GlobalSign Root CA - R2 ------BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 -MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL -v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 -eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq -tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd -C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa -zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB -mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH -V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n -bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG -3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs -J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO -291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS -ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd -AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 -TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== ------END CERTIFICATE----- - -# GlobalSign Root CA - R3 ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 -MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 -RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT -gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm -KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd -QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ -XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o -LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU -RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp -jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK -6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX -mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs -Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH -WD9f ------END CERTIFICATE----- - -# Global Chambersign Root - 2008 ------BEGIN CERTIFICATE----- -MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD -VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 -IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 -MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD -aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx -MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy -cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG -A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl -BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI -hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed -KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 -G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 -zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 -ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG -HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 -Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V -yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e -beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r -6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh -wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog -zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW -BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr -ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp -ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk -cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt -YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC -CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow -KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI -hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ -UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz -X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x -fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz -a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd -Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd -SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O -AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso -M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge -v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z -09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B ------END CERTIFICATE----- - -# Go Daddy Class 2 CA ------BEGIN CERTIFICATE----- -MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh -MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE -YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 -MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo -ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg -MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN -ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA -PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w -wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi -EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY -avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ -YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE -sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h -/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 -IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD -ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy -OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P -TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ -HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER -dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf -ReYNnyicsbkqWletNw+vHX/bvZ8= ------END CERTIFICATE----- - -# Go Daddy Root Certificate Authority - G2 ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT -EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp -ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz -NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH -EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE -AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD -E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH -/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy -DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh -GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR -tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA -AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX -WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu -9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr -gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo -2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI -4uJEvlz36hz1 ------END CERTIFICATE----- - -# Hellenic Academic and Research Institutions ECC RootCA 2015 ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN -BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl -bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv -b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ -BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj -YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 -MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 -dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg -QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa -jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi -C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep -lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof -TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -# Hellenic Academic and Research Institutions RootCA 2011 ------BEGIN CERTIFICATE----- -MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix -RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p -YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw -NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK -EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl -cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz -dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ -fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns -bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD -75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP -FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV -HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp -5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu -b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA -A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p -6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 -TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 -dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys -Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI -l7WdmplNsDz4SgCbZN2fOUvRJ9e4 ------END CERTIFICATE----- - -# Hellenic Academic and Research Institutions RootCA 2015 ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix -DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k -IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT -N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v -dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG -A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh -ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx -QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA -4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 -AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 -4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C -ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV -9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD -gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 -Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq -NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko -LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd -ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I -XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI -M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot -9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V -Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea -j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh -X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ -l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf -bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 -pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK -e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 -vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -# Hongkong Post Root CA 1 ------BEGIN CERTIFICATE----- -MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx -FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg -Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG -A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr -b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ -jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn -PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh -ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 -nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h -q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED -MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC -mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 -7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB -oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs -EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO -fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi -AmvZWg== ------END CERTIFICATE----- - -# ISRG Root X1 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -# IdenTrust Commercial Root CA 1 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu -VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw -MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw -JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT -3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU -+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp -S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 -bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi -T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL -vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK -Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK -dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT -c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv -l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N -iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD -ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt -LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 -nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 -+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK -W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT -AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq -l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG -4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ -mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A -7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -# IdenTrust Public Sector Root CA 1 ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu -VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN -MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 -MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 -ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy -RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS -bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF -/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R -3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw -EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy -9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V -GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ -2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV -WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD -W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN -AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV -DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 -TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G -lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW -mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df -WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 -+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ -tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA -GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv -8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -# Izenpe.com ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 -MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 -ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD -VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j -b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq -scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO -xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H -LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX -uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD -yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ -JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q -rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN -BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L -hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB -QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ -HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu -Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg -QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB -BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA -A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb -laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 -awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo -JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw -LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT -VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk -LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb -UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ -QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ -naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls -QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -# LuxTrust Global Root 2 ------BEGIN CERTIFICATE----- -MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL -BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV -BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw -MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B -LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F -ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem -hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1 -EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn -Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4 -zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ -96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m -j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g -DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+ -8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j -X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH -hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB -KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0 -Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT -+Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL -BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9 -BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO -jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9 -loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c -qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+ -2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/ -JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre -zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf -LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+ -x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6 -oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr ------END CERTIFICATE----- - -# Microsec e-Szigno Root CA 2009 ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G -CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y -OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx -FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp -Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP -kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc -cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U -fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 -N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC -xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 -+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM -Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG -SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h -mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk -ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c -2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t -HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -# NetLock Arany (Class Gold) Főtanúsítvány ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG -EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 -MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl -cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR -dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB -pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM -b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm -aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz -IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT -lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz -AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 -VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG -ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 -BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG -AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M -U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh -bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C -+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F -uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 -XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -# Network Solutions Certificate Authority ------BEGIN CERTIFICATE----- -MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi -MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu -MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp -dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV -UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO -ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz -c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP -OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl -mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF -BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 -qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw -gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB -BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu -bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp -dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 -6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ -h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH -/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv -wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN -pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey ------END CERTIFICATE----- - -# OISTE WISeKey Global Root GA CA ------BEGIN CERTIFICATE----- -MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB -ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly -aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl -ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w -NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G -A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD -VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX -SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR -VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 -w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF -mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg -4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 -4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw -EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx -SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 -ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 -vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa -hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi -Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ -/L7fCg0= ------END CERTIFICATE----- - -# OISTE WISeKey Global Root GB CA ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt -MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg -Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i -YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x -CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG -b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 -HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx -WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX -1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk -u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P -99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r -M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB -BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh -cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 -gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO -ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf -aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -# OpenTrust Root CA G1 ------BEGIN CERTIFICATE----- -MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA -MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w -ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw -MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU -T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b -wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX -/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0 -77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP -uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx -p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx -Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2 -TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W -G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw -vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY -EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1 -2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw -DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E -PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf -gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS -FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0 -V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P -XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I -i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t -TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91 -09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky -Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ -AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj -1oxx ------END CERTIFICATE----- - -# OpenTrust Root CA G2 ------BEGIN CERTIFICATE----- -MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA -MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w -ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw -MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU -T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh -/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e -CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6 -1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE -FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS -gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X -G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy -YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH -vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4 -t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/ -gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3 -5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w -DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz -Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0 -nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT -RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT -wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2 -t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa -TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2 -o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU -3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA -iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f -WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM -S1IK ------END CERTIFICATE----- - -# OpenTrust Root CA G3 ------BEGIN CERTIFICATE----- -MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx -CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U -cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow -QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl -blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm -3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d -oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5 -DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK -BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q -j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx -4nxp5V2a+EEfOzmTk51V6s2N8fvB ------END CERTIFICATE----- - -# QuoVadis Root CA ------BEGIN CERTIFICATE----- -MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC -TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz -MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw -IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR -dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp -li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D -rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ -WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug -F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU -xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC -Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv -dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw -ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl -IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh -c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy -ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh -Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI -KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T -KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq -y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p -dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD -VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk -fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 -7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R -cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y -mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW -xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK -SnQ2+Q== ------END CERTIFICATE----- - -# QuoVadis Root CA 1 G3 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 -MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV -wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe -rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 -68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh -4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp -UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o -abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc -3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G -KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt -hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO -Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt -zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD -ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 -cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN -qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 -YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv -b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 -8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k -NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj -ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp -q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt -nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE----- - -# QuoVadis Root CA 2 ------BEGIN CERTIFICATE----- -MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x -GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv -b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV -BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W -YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa -GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg -Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J -WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB -rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp -+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 -ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i -Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz -PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og -/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH -oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI -yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud -EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 -A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL -MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT -ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f -BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn -g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl -fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K -WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha -B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc -hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR -TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD -mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z -ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y -4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza -8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE----- - -# QuoVadis Root CA 2 G3 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -# QuoVadis Root CA 3 ------BEGIN CERTIFICATE----- -MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x -GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv -b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV -BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W -YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM -V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB -4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr -H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd -8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv -vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT -mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe -btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc -T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt -WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ -c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A -4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD -VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG -CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 -aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 -aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu -dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw -czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G -A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC -TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg -Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 -7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem -d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd -+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B -4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN -t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x -DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 -k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s -zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j -Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT -mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK -4SVhM7JZG+Ju1zdXtg2pEto= ------END CERTIFICATE----- - -# QuoVadis Root CA 3 G3 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -# SSL.com EV Root Certification Authority ECC ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -# SSL.com EV Root Certification Authority RSA R2 ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV -BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE -CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy -MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G -A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD -DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq -M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf -OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa -4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 -HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR -aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA -b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ -Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV -PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO -pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu -UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY -MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 -9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW -s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 -Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg -cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM -79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz -/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt -ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm -Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK -QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ -w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi -S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 -mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -# SSL.com Root Certification Authority ECC ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz -WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 -b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS -b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI -7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg -CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -# SSL.com Root Certification Authority RSA ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -# SZAFIR ROOT CA2 ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 -ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw -NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L -cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg -Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN -QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT -3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw -3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 -3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 -BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN -XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF -AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw -8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG -nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP -oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy -d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg -LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -# SecureSign RootCA11 ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr -MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG -A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 -MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp -Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD -QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz -i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 -h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV -MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 -UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni -8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC -h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD -VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB -AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm -KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ -X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr -QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 -pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN -QSdJQO7e5iNEOdyhIta6A/I= ------END CERTIFICATE----- - -# SecureTrust CA ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz -MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv -cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz -Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO -0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao -wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj -7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS -8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT -BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg -JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 -6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ -3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm -D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS -CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -# Secure Global CA ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx -MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg -Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ -iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa -/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ -jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI -HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 -sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w -gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw -KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG -AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L -URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO -H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm -I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY -iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- - -# Security Communication RootCA2 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX -DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy -dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj -YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV -OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr -zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM -VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ -hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO -ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw -awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs -OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF -coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc -okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 -t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy -1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ -SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -# Security Communication Root CA ------BEGIN CERTIFICATE----- -MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY -MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t -dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 -WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD -VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 -9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ -DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 -Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N -QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ -xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G -A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG -kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr -Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 -Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU -JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot -RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== ------END CERTIFICATE----- - -# Sonera Class 2 Root CA ------BEGIN CERTIFICATE----- -MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP -MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx -MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV -BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o -Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt -5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s -3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej -vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu -8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw -DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG -MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil -zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ -3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD -FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 -Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 -ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M ------END CERTIFICATE----- - -# Staat der Nederlanden EV Root CA ------BEGIN CERTIFICATE----- -MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y -MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg -TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS -b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS -M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC -UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d -Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p -rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l -pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb -j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC -KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS -/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X -cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH -1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP -px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 -MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI -eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u -2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS -v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC -wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy -CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e -vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 -Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa -Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL -eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 -FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc -7uzXLg== ------END CERTIFICATE----- - -# Staat der Nederlanden Root CA - G2 ------BEGIN CERTIFICATE----- -MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX -DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl -ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv -b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 -qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp -uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU -Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE -pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp -5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M -UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN -GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy -5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv -6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK -eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 -B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ -BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov -L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG -SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS -CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen -5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 -IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK -gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL -+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL -vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm -bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk -N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC -Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z -ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== ------END CERTIFICATE----- - -# Staat der Nederlanden Root CA - G3 ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX -DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl -ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv -b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP -cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW -IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX -xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy -KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR -9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az -5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 -6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 -Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP -bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt -BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt -XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd -INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD -U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp -LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 -Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp -gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh -/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw -0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A -fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq -4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR -1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ -QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM -94B7IWcnMFk= ------END CERTIFICATE----- - -# Starfield Class 2 CA ------BEGIN CERTIFICATE----- -MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl -MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp -U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw -NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE -ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp -ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 -DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf -8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN -+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 -X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa -K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA -1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G -A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR -zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 -YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD -bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w -DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 -L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D -eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl -xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp -VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY -WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= ------END CERTIFICATE----- - -# Starfield Root Certificate Authority - G2 ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -# Starfield Services Root Certificate Authority - G2 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs -ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD -VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy -ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy -dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p -OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 -8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K -Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe -hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk -6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q -AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI -bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB -ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z -qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn -0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN -sSi6 ------END CERTIFICATE----- - -# SwissSign Gold CA - G2 ------BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln -biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF -MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT -d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 -76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ -bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c -6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE -emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd -MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt -MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y -MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y -FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi -aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM -gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB -qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 -lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn -8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov -L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 -45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO -UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 -O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC -bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv -GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a -77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC -hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 -92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp -Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w -ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt -Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE----- - -# SwissSign Silver CA - G2 ------BEGIN CERTIFICATE----- -MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE -BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu -IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow -RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY -U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv -Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br -YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF -nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH -6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt -eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ -c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ -MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH -HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf -jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 -5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB -rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU -F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c -wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 -cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB -AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp -WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 -xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ -2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ -IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 -aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X -em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR -dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ -OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ -hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy -tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u ------END CERTIFICATE----- - -# T-TeleSec GlobalRoot Class 2 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - -# T-TeleSec GlobalRoot Class 3 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE----- - -# TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx -GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp -bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w -KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 -BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy -dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG -EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll -IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU -QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT -TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg -LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 -a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr -LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr -N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X -YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ -iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f -AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH -V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf -IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 -lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c -8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf -lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -# TWCA Global Root CA ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE----- - -# TWCA Root Certification Authority ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES -MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU -V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz -WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO -LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE -AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH -K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX -RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z -rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx -3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq -hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC -MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls -XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D -lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn -aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ -YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -# Taiwan GRCA ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ -MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow -PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR -IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q -gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy -yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts -F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 -jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx -ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC -VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK -YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH -EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN -Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud -DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE -MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK -UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ -TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf -qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK -ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE -JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 -hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 -EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm -nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX -udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz -ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe -LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl -pYYsfPQS ------END CERTIFICATE----- - -# TeliaSonera Root CA v1 ------BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw -NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv -b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD -VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F -VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 -7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X -Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ -/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs -81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm -dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe -Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu -sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 -pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs -slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ -arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD -VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG -9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl -dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj -TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed -Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 -Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI -OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 -vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW -t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn -HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx -SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE----- - -# TrustCor ECA-1 ------BEGIN CERTIFICATE----- -MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD -VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk -MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U -cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y -IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV -BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw -IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy -dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig -RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb -3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA -BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 -3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou -owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ -wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF -ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf -BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ -MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv -civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 -AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F -hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 -soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI -WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi -tJ/X5g== ------END CERTIFICATE----- - -# TrustCor RootCert CA-1 ------BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD -VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk -MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U -cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y -IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB -pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h -IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG -A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU -cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid -RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V -seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme -9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV -EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW -hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ -DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD -ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I -/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf -ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ -yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts -L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN -zl/HHk484IkzlQsPpTLWPFp5LBk= ------END CERTIFICATE----- - -# TrustCor RootCert CA-2 ------BEGIN CERTIFICATE----- -MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV -BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw -IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy -dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig -Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk -MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg -Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD -VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy -dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ -QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq -1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp -2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK -DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape -az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF -3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 -oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM -g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 -mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh -8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd -BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U -nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw -DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX -dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ -MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL -/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX -CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa -ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW -2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 -N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 -Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB -As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp -5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu -1uwJ ------END CERTIFICATE----- - -# Trustis FPS Root CA ------BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL -ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx -MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc -MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ -AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH -iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj -vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA -0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB -OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ -BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E -FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 -GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW -zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 -1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE -f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F -jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN -ZetX2fNXlrtIzYE= ------END CERTIFICATE----- - -# TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 ------BEGIN CERTIFICATE----- -MIIEJzCCAw+gAwIBAgIHAI4X/iQggTANBgkqhkiG9w0BAQsFADCBsTELMAkGA1UE -BhMCVFIxDzANBgNVBAcMBkFua2FyYTFNMEsGA1UECgxEVMOcUktUUlVTVCBCaWxn -aSDEsGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkg -QS7Fni4xQjBABgNVBAMMOVTDnFJLVFJVU1QgRWxla3Ryb25payBTZXJ0aWZpa2Eg -SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSBINTAeFw0xMzA0MzAwODA3MDFaFw0yMzA0 -MjgwODA3MDFaMIGxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYD -VQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 -dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBF -bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg1MIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCUZ4WWe60ghUEoI5RHwWrom -/4NZzkQqL/7hzmAD/I0Dpe3/a6i6zDQGn1k19uwsu537jVJp45wnEFPzpALFp/kR -Gml1bsMdi9GYjZOHp3GXDSHHmflS0yxjXVW86B8BSLlg/kJK9siArs1mep5Fimh3 -4khon6La8eHBEJ/rPCmBp+EyCNSgBbGM+42WAA4+Jd9ThiI7/PS98wl+d+yG6w8z -5UNP9FR1bSmZLmZaQ9/LXMrI5Tjxfjs1nQ/0xVqhzPMggCTTV+wVunUlm+hkS7M0 -hO8EuPbJbKoCPrZV4jI3X/xml1/N1p7HIL9Nxqw/dV8c7TKcfGkAaZHjIxhT6QID -AQABo0IwQDAdBgNVHQ4EFgQUVpkHHtOsDGlktAxQR95DLL4gwPswDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ5FdnsX -SDLyOIspve6WSk6BGLFRRyDN0GSxDsnZAdkJzsiZ3GglE9Rc8qPoBP5yCccLqh0l -VX6Wmle3usURehnmp349hQ71+S4pL+f5bFgWV1Al9j4uPqrtd3GqqpmWRgqujuwq -URawXs3qZwQcWDD1YIq9pr1N5Za0/EKJAWv2cMhQOQwt1WbZyNKzMrcbGW3LM/nf -peYVhDfwwvJllpKQd/Ct9JDpEXjXk4nAPQu6KfTomZ1yju2dL+6SfaHx/126M2CF -Yv4HAqGEVka+lgqaE9chTLd8B59OTj+RdPsnnRHM3eaxynFNExc5JsUpISuTKWqW -+qtB4Uu2NQvAmxU= ------END CERTIFICATE----- - -# USERTrust ECC Certification Authority ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL -MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl -eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT -JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT -Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg -VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo -I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng -o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G -A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB -zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW -RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -# USERTrust RSA Certification Authority ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB -iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl -cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV -BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw -MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV -BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B -3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY -tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ -Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 -VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT -79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 -c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT -Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l -c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee -UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE -Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF -Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO -VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 -ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs -8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR -iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze -Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ -XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ -qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB -VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB -L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG -jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -# VeriSign Class 3 Public Primary Certification Authority - G4 ------BEGIN CERTIFICATE----- -MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG -A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp -U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg -SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln -biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm -GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve -fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ -aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj -aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW -kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC -4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga -FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== ------END CERTIFICATE----- - -# VeriSign Class 3 Public Primary Certification Authority - G5 ------BEGIN CERTIFICATE----- -MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 -nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex -t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz -SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG -BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ -rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ -NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH -BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy -aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv -MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE -p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y -5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK -WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ -4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N -hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq ------END CERTIFICATE----- - -# VeriSign Universal Root Certification Authority ------BEGIN CERTIFICATE----- -MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB -vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W -ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe -Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX -MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 -IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y -IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh -bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF -9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH -H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H -LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN -/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT -rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud -EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw -WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs -exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud -DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 -sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ -seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz -4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ -BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR -lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 -7M2CYfE45k+XmCpajQ== ------END CERTIFICATE----- - -# Verisign Class 3 Public Primary Certification Authority - G3 ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b -N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t -KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu -kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm -CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ -Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu -imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te -2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe -DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC -/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p -F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt -TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== ------END CERTIFICATE----- - -# Visa eCommerce Root ------BEGIN CERTIFICATE----- -MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr -MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl -cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv -bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw -CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h -dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l -cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h -2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E -lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV -ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq -299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t -vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL -dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF -AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR -zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 -LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd -7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw -++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt -398znM/jra6O1I7mT1GvFpLgXPYHDw== ------END CERTIFICATE----- - -# XRamp Global CA Root ------BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB -gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk -MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY -UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx -NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 -dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy -dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 -38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP -KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q -DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 -qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa -JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi -PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P -BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs -jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 -eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD -ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR -vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt -qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa -IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy -i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ -O+7ETPTsJ3xCwnR8gooJybQDJbw= ------END CERTIFICATE----- - -# certSIGN ROOT CA ------BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT -AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD -QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP -MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do -0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ -UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d -RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ -OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv -JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C -AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O -BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ -LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY -MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ -44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I -Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw -i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN -9u6wWk5JRFRYX0KD ------END CERTIFICATE----- - -# ePKI Root Certification Authority ------BEGIN CERTIFICATE----- -MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe -Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw -IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL -SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH -SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh -ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X -DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 -TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ -fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA -sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU -WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS -nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH -dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip -NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC -AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF -MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH -ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB -uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl -PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP -JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ -gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 -j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 -5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB -o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS -/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z -Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE -W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D -hNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE----- - -# thawte Primary Root CA ------BEGIN CERTIFICATE----- -MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB -qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV -BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw -NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j -LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG -A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs -W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta -3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk -6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 -Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J -NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP -r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU -DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz -YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX -xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 -/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ -LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 -jVaMaA== ------END CERTIFICATE----- - -# thawte Primary Root CA - G2 ------BEGIN CERTIFICATE----- -MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp -IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi -BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw -MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh -d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig -YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v -dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ -BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 -papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K -DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 -KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox -XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== ------END CERTIFICATE----- - -# thawte Primary Root CA - G3 ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB -rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV -BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa -Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl -LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u -MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl -ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm -gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 -YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf -b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 -9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S -zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk -OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV -HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA -2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW -oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu -t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c -KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM -m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu -MdRAGmI0Nj81Aa6sY6A= ------END CERTIFICATE----- diff --git a/src/ltrace/ltrace/assets_utils.py b/src/ltrace/ltrace/assets_utils.py index 8c30405..fc03553 100644 --- a/src/ltrace/ltrace/assets_utils.py +++ b/src/ltrace/ltrace/assets_utils.py @@ -2,57 +2,65 @@ import json +ROOT = Path(__file__).resolve().absolute().parent / "assets" +PUBLIC = ROOT / "public" +PRIVATE = ROOT / "private" + + def get_asset(asset_name): - """Returns the asset's absolute Path given a relative path from this dir.""" + """Returns the asset's absolute Path given a relative path from either public or private dir. + Do not include 'public' or 'private' in the path. + """ if isinstance(asset_name, str): asset_name = Path(asset_name) - absolute_path = Path(__file__).resolve().absolute().parent / "assets" / asset_name - if not absolute_path.exists(): - raise RuntimeError("Invalid asset: {}".format(absolute_path)) - - return absolute_path + private_path = PRIVATE / asset_name + if private_path.exists(): + return private_path + public_path = PUBLIC / asset_name + if public_path.exists(): + return public_path -def get_trained_model(model_name): - return get_asset(Path("trained_models") / Path(model_name)) + return None -def get_trained_models(environment=None): - models_path = Path(__file__).resolve().absolute().parent / "assets" / "trained_models" - - if environment: - models_path = models_path / environment +def get_trained_models(environment): + models_path = Path(__file__).resolve().absolute().parent / "assets" / "models" extensions = [".h5", ".pth"] - models = [file for file in models_path.glob("**/*") if file.suffix in extensions] + models = [] + for root_path in (PUBLIC, PRIVATE): + models_path = root_path / environment + models += [file for file in models_path.glob("**/*") if file.suffix in extensions] return models def get_trained_models_with_metadata(environment): - models_path = Path(__file__).resolve().absolute().parent / "assets" / "trained_models" - models_path = models_path / environment - models = [] - for subdir in models_path.iterdir(): - if not subdir.is_dir(): + for root_path in (PUBLIC, PRIVATE): + models_path = root_path / environment + if not models_path.is_dir(): continue - base = subdir.name - pth = subdir / f"{base}.pth" + for subdir in models_path.iterdir(): + if not subdir.is_dir(): + continue + base = subdir.name + pth = subdir / f"model.pth" - assert pth.exists(), f"Directory {base} exists but file {pth} does not exist" + assert pth.exists(), f"Directory {base} exists but file {pth} does not exist" - models.append(subdir) + models.append(subdir) return models def get_metadata(model_dir): model_dir = Path(model_dir) - with open(model_dir / f"{model_dir.name}.json") as f: + with open(model_dir / f"meta.json") as f: metadata = json.load(f) return metadata def get_pth(model_dir): model_dir = Path(model_dir) - return model_dir / f"{model_dir.name}.pth" + return model_dir / f"model.pth" diff --git a/src/ltrace/ltrace/constants.py b/src/ltrace/ltrace/constants.py index dae96ed..3cbdc07 100644 --- a/src/ltrace/ltrace/constants.py +++ b/src/ltrace/ltrace/constants.py @@ -17,3 +17,14 @@ class ImageLogConst: # The default layout ID value for the ImageLogData module. DEFAULT_LAYOUT_ID_START_VALUE = 15000 + + +class ImageLogInpaintConst: + """ + Constants for ImageLogInpaint module + """ + + SEGMENT_ID = "Segment_1" + TEMP_SEGMENTATION_NAME = "Inpaint_Segmentation_ImageLog" + TEMP_LABEL_MAP_NAME = "Inpaint_Mask_ImageLog" + TEMP_VOLUME_NAME = "Inpaint_Volume_ImageLog" diff --git a/src/ltrace/ltrace/file_utils.py b/src/ltrace/ltrace/file_utils.py index 932c9f0..052380b 100644 --- a/src/ltrace/ltrace/file_utils.py +++ b/src/ltrace/ltrace/file_utils.py @@ -85,9 +85,6 @@ def detect_csv_file_delimiter(filepath, encoding, whitelist=[",", ";"]): # read in non binary with encoding, to detect delimiter with open(filepath, "r", encoding=encoding) as file: data = file.readlines(50) - if not data: - return None - counters = {} for delimiter in whitelist: count = number_of_delimiters_per_line(data, delimiter) diff --git a/src/ltrace/ltrace/image/core_box/core_boxes_image_file.py b/src/ltrace/ltrace/image/core_box/core_boxes_image_file.py index e1ad84f..aa286c6 100644 --- a/src/ltrace/ltrace/image/core_box/core_boxes_image_file.py +++ b/src/ltrace/ltrace/image/core_box/core_boxes_image_file.py @@ -1,5 +1,5 @@ from collections import namedtuple -from ltrace.assets_utils import get_trained_model +from ltrace.assets_utils import get_asset from ltrace.image.core_box.core_box import CoreBox from ltrace.slicer.helpers import concatenateImageArrayVertically, resizeRgbArray from ltrace.units import safe_atof @@ -306,7 +306,7 @@ def __extract_cores_info(self, img): from ltrace.image.segmentation import TF_RGBImageArrayBinarySegmenter CoreBoxesImageFile.BinarySegmenterModel = TF_RGBImageArrayBinarySegmenter( - get_trained_model("unet-binary-segop.h5"), gpuEnabled=self.__gpuEnabled + get_asset("unet-binary-segop.h5"), gpuEnabled=self.__gpuEnabled ) for core_cut in self.__split_cores(img, number_of_cores): diff --git a/src/ltrace/ltrace/image/las.py b/src/ltrace/ltrace/image/las.py new file mode 100644 index 0000000..7bdc767 --- /dev/null +++ b/src/ltrace/ltrace/image/las.py @@ -0,0 +1,230 @@ +import numpy as np +import os +import lasio +import slicer +from ltrace.slicer.helpers import ( + getVolumeNullValue, + arrayFromVisibleSegmentsBinaryLabelmap, +) +from ImageLogExportLib.ImageLogCSV import _arrayPartsFromNode +from pathlib import Path +import pandas as pd +import datetime +import re +import logging +from dataclasses import dataclass +from DLISImportLib.DLISImportLogic import WELL_NAME_TAG, UNITS_TAG + +import ltrace.image.lasGeologFile as lasGeologFile + + +def retrieve_depth_curve(node_list): + def extract_depth_info_from_node(node): + lasdata, depths = _extract_las_data_from_node(node) + data = lasdata["data"].squeeze() + + # Slicer world is in mm. Converting to meters: + step = lasdata["step"] / 1000.0 + origin = lasdata["origin"] / 1000.0 + + if isinstance(node, slicer.vtkMRMLTableNode): + return depths + else: + min_depth = -1 * origin + max_depth = min_depth + step * (data.shape[0]) + depths = np.linspace(min_depth, max_depth, data.shape[0]) + return depths + + def check_single_depth_list(depths_all): + depth_disparity_tolerance = 5e-02 + for i in range(len(depths_all) - 1): + subtr = np.subtract(depths_all[i], depths_all[i + 1]) + assert len(depths_all[0][abs(subtr) > depth_disparity_tolerance]) == 0 + + depths_all = [[]] + depths = [] + # We prefer to read the depths from a table instead of calculating the depths from a volume... + for i, node in enumerate(node_list): + depths_all.append(extract_depth_info_from_node(node)) + if isinstance(node, slicer.vtkMRMLTableNode) and len(depths) == 0: + depths = depths_all[-1].copy() + # ... but we calculate them from a volume node if there's no table in the scene + if not len(depths): + for i, node in enumerate(node_list): + depths_all.append(extract_depth_info_from_node(node)) + if len(depths) == 0: + depths = depths_all[-1].copy() + + depths_all = depths_all[1:] # getting rid of first empty element allocated at declaration + + try: + check_single_depth_list(depths_all) + except: + raise RuntimeError( + f"Error: Can't export to a LAS file curves obtained in different depths. If you imported from DLIS, try to export different frames or logical files to different LAS files." + ) + + return depths + + +def export_las( + node_list, + output_path, + version=2, + well_name="", + well_id="", + field_name="", + company_name="", + producer_name="", + file_id="", +): + lasfile = lasGeologFile.lasGeologFile() # lasio.LASFile + lasfile.well.DATE = datetime.date.today().strftime("%Y-%m-%d %H:%M:%S") + lasfile.well.WELL = well_name + lasfile.well.UWY = well_id + lasfile.well.FLD = field_name + lasfile.well.COMP = company_name # the company which the log was produced for + lasfile.well.SRVC = producer_name # DLIS: "producer's name"; LAS: The logging company + lasfile.other = "Generated by GeoSlicer" + + # Retrieve the depths one single time + depths = retrieve_depth_curve(node_list) + + # (When appending the DEPT curve, WELL.STRT and WELL.STEP will be set automatically by lasio) + if depths.any(): + lasfile.append_curve("DEPT", depths, unit="m", descr=" ") + else: + raise RuntimeError( + f"Error exporting to LAS: not possible to get depths. Please check if you selected valid nodes to export." + ) + + # Adding the other data curves extracted from the nodes + las_well_name = "" + count_well = 0 + for i, node in enumerate(node_list): + try: + las_data, depths = _extract_las_data_from_node(node) + except: + raise RuntimeError(f"Error exporting to LAS: can't extract data from node {node.GetName()}") + las_info = extract_las_info_from_node(node) + + # We expect a single well per file - check that + lasfile.well.WELL = las_info["well_name"] + if las_info["well_name"] != las_well_name: + las_well_name = las_info["well_name"] + count_well += 1 + if count_well == 2: + raise RuntimeError("Error exporting to LAS. You can't export nodes from different Wells to the same file.") + + add_curve(lasfile, las_data, las_info) + + lasfile.write(output_path, version=version) + + +# TODO - MUSA-76 Consider condensing las_data and las_info in a single dataclass +def add_curve(lasfile, las_data, las_info): + + data = las_data["data"].squeeze() + + data[data == las_info["null_value"]] = -999.25 # TODO MUSA-75 Update invalid data handling policy + + if data.ndim > 1: + if data.ndim == 1: + image_width = 1 + else: + image_width = data.shape[1] + for i in range(0, image_width): + curve_data = data[:, i] + lasfile.append_curve( + (las_info["data_name"]).strip() + "[{0}]".format(i + 1), + curve_data, + las_info["units"], + descr=f"{{AF}}", # {AF} stands for "array of floats" + ) + else: + lasfile.append_curve(las_info["data_name"], data, las_info["units"], descr=" ") + + +def _extract_las_data_from_node(node): + las_data = {} + depths = [] + if isinstance(node, slicer.vtkMRMLSegmentationNode): + las_data["data"], spacing, origin = arrayFromVisibleSegmentsBinaryLabelmap(node) + las_data["step"] = spacing[2] + las_data["origin"] = origin[2] + elif isinstance(node, slicer.vtkMRMLTableNode): + depths, las_data["data"] = _arrayPartsFromNode(node) + las_data["step"] = depths[1] - depths[0] + las_data["origin"] = depths[0] + else: + las_data["data"] = slicer.util.arrayFromVolume(node) + las_data["step"] = node.GetSpacing()[2] + las_data["origin"] = node.GetOrigin()[2] + + return las_data, depths + + +def extract_las_info_from_node(node): + + las_info = {} + + # units + units_search = re.search(r"\[(.*?)\]", node.GetName()) + units_from_name = units_search.group(1) if units_search else "NONE" + units = units_from_name + + if node.GetAttribute(UNITS_TAG) is not None: + units = node.GetAttribute(UNITS_TAG) + if units_from_name == "NONE": + logging.info( + f"Node name ({node.GetName()}) doesn't include its units ({node.GetAttribute(UNITS_TAG)}) in it." + ) + elif node.GetAttribute(UNITS_TAG) != units_from_name: + logging.warning( + f"Units informed in {node.GetName()} ({node.GetAttribute(UNITS_TAG)}) metadata are different from the units implied by the node name ({units_from_name}). {node.GetAttribute(UNITS_TAG)} will be considered as the units." + ) + else: + units = units_from_name + if units_from_name == "NONE": + logging.warning(f"No units found for {node.GetName()}. They'll be set to value 'NONE'") + + las_info["units"] = units + + # well name + well_from_node_name = node.GetName().split("_")[0] if len(node.GetName().split("_")) > 1 else "" + if node.GetAttribute(WELL_NAME_TAG) is not None: + las_info["well_name"] = node.GetAttribute(WELL_NAME_TAG) + if well_from_node_name == "": + logging.info( + f"Node name ({node.GetName()}) doesn't have the well name ({node.GetAttribute(WELL_NAME_TAG)}) prepended to it." + ) + elif node.GetAttribute(WELL_NAME_TAG) != well_from_node_name: + logging.warning( + f"Well name informed in {node.GetName()} ({node.GetAttribute(WELL_NAME_TAG)}) metadata is different from the well name implied by the node name ({well_from_node_name}). {node.GetAttribute(WELL_NAME_TAG)} will be considered as the well name." + ) + else: + las_info["well_name"] = well_from_node_name + if well_from_node_name == "": + logging.warning(f"No well name found for {node.GetName()}.") + + las_info["data_name"] = node.GetName().replace("[" + units + "]", "") + new_data_name = las_info["data_name"] + if node.GetAttribute(WELL_NAME_TAG): + new_data_name = las_info["data_name"].replace(node.GetAttribute(WELL_NAME_TAG) + "_", "") + if len(new_data_name) == 0: + new_data_name = node.GetAttribute(WELL_NAME_TAG) + las_info["data_name"] = new_data_name + + las_info["null_value"] = ( + 0 + if isinstance(node, slicer.vtkMRMLLabelMapVolumeNode) or isinstance(node, slicer.vtkMRMLSegmentationNode) + else getVolumeNullValue(node) + ) + + subject_hierarchy_node = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + item_parent = subject_hierarchy_node.GetItemParent(subject_hierarchy_node.GetItemByDataNode(node)) + directory_name = subject_hierarchy_node.GetItemName(item_parent) + + las_info["frame_name"] = directory_name + + return las_info diff --git a/src/ltrace/ltrace/image/lasGeologFile.py b/src/ltrace/ltrace/image/lasGeologFile.py new file mode 100644 index 0000000..55e0516 --- /dev/null +++ b/src/ltrace/ltrace/image/lasGeologFile.py @@ -0,0 +1,330 @@ +# Imports to customize LASFile.write +from lasio.las import * +import lasio + +# Imports to customize lasio.writer +import logging +import textwrap + +import numpy as np + +from copy import deepcopy + +from lasio.las_items import HeaderItem, CurveItem, SectionItems, OrderedDict +from lasio import defaults +from lasio import exceptions + +logger = logging.getLogger(__name__) + + +# Inherits LASFile so we call lasGeologWrite +class lasGeologFile(lasio.las.LASFile): + def write(self, file_ref, **kwargs): + opened_file = False + if isinstance(file_ref, basestring) and not hasattr(file_ref, "write"): + opened_file = True + file_ref = open(file_ref, "w") + lasGeoLogWrite(self, file_ref, **kwargs) + if opened_file: + file_ref.close() + + +# Custom lasio.writer.write +# It's mostly duplicated code from the original lasio, but may be the base for a full LAS 3.0 support +# Changes: +# - Accepts value 3 as version argument +# - When value==3: +# - VERS header item is written as 3 +# - Parameter section string is "~Parameter " (instead of 2.0 "~Params ") +# From the LAS 3.0 spec, the indication that a curve is 2-D is made via specific strings in the mnemonics names and descritptions. +# These are set in the src/ltrace/image/las.py, but in the future they may be set here +# - Specifically, the mnemonics have to have an index surrounded by []'s appended to their names, +# and their description string (which is surrounded by {}'s) has to start with "AF" (array of floats) +def lasGeoLogWrite( + las, + file_object, + version=None, + wrap=None, + STRT=None, + STOP=None, + STEP=None, + fmt="%.5f", + column_fmt=None, + len_numeric_field=None, + lhs_spacer=" ", + spacer=" ", + data_width=79, + header_width=60, + data_section_header="~ASCII", + mnemonics_header=False, +): + """Write a LAS files. + + Arguments: + las (:class:`lasio.LASFile`) + file_object (file-like object open for writing): output + version (float or None): version of written file, either 1.2 or 2. + If this is None, ``las.version.VERS.value`` will be used. + wrap (bool or None): whether to wrap the output data section. + If this is None, ``las.version.WRAP.value`` will be used. + STRT (float or None): value to use as STRT (note the data will not + be clipped). If this is None, the data value in the first column, + first row will be used. + STOP (float or None): value to use as STOP (note the data will not + be clipped). If this is None, the data value in the first column, + last row will be used. + STEP (float or None): value to use as STEP (note the data will not + be resampled and/or interpolated). If this is None, the STEP will + be estimated from the first two rows of the first column. + fmt (str): Python string formatting operator for numeric data to be + used. + column_fmt (dict or None): use this to set a different format string + for specific columns from the data ndarray. E.g. to use ``'%.3f'`` + for the depth column and ``'%.2f'`` for all the other columns, + you would use ``fmt='%.2f', column_fmt={0: '%.3f'}``. + len_numeric_field (int): width of each numeric field column (must be + greater than than all the formatted numeric values in the file). + If it is None, the maximum necessary value will be used automatically + (i.e. all columns will have the same width). If it is -1, then + the columns will not have consistent widths. You can combine + -1 with the *fmt* keyword argument to control column widths closely. + data_width (79): width of data field in characters + lhs_spacer (str): string which goes on left hand side of data + section - by default it is `" "`. + spacer (str): string which goes between each column of the data section + data_section_header (str): default "~ASCII" + mnemonics_header (bool): include mnemonic curve names in the + data_section_header at the top of data section + + Creating an output file is not the only side-effect of this function. It + will also modify the STRT, STOP and STEP HeaderItems so that they correctly + reflect the ~Data section's units and the actual first, last, and interval + values. + + However, passing a version to this write() function only changes the version + of the object written to. Example: las.write(myfile, version=2). + Lasio's internal-las-object version will remain separate and defined by + las.version.VERS.value + + You should avoid calling this function directly - instead use the + :meth:`lasio.LASFile.write` method. + + """ + if column_fmt is None: + column_fmt = {} + if wrap is None: + wrap = las.version["WRAP"] == "YES" + elif wrap is True: + las.version["WRAP"] = HeaderItem("WRAP", "", "YES", "Multiple lines per depth step") + elif wrap is False: + las.version["WRAP"] = HeaderItem("WRAP", "", "NO", "One line per depth step") + lines = [] + + version_section_to_write = deepcopy(las.version) + + assert version in (1.2, 2, 3, None) + if version is None: + version = las.version["VERS"].value + if version == 1.2: + version_section_to_write.VERS = HeaderItem("VERS", "", 1.2, "CWLS LOG ASCII STANDARD - VERSION 1.2") + elif int(version) == 2: + version_section_to_write.VERS = HeaderItem("VERS", "", 2.0, "CWLS log ASCII Standard - VERSION 2.0") + elif int(version) == 3: + version_section_to_write.VERS = HeaderItem("VERS", "", 3.0, "CWLS log ASCII Standard - VERSION 3.0") + + # ------------------------------------------------------------------------- + # If an initial curve index was not read from a las file (las.index_initial) + # or the curve index has changed during processing + # or if the STOP value doesn't match the final index value + # then update the step variables before writing to a new las file object. + # ------------------------------------------------------------------------- + index_changed = False + stop_is_different = False + + if las.index_initial is not None: + index_changed = not np.array_equal(las.index_initial, las.index) + stop_is_different = las.index_initial[-1] != las.well.STOP.value + else: + index_changed = True + + if index_changed or stop_is_different: + las.update_start_stop_step(STRT, STOP, STEP) + + las.update_units_from_index_curve() + + # Write each section. + # get_formatter_function ( ** get_section_widths ) + + # ~Version + logger.debug("LASFile.write Version section, Version: %s" % (version)) + lines.append("~Version ".ljust(header_width, "-")) + order_func = lasio.writer.get_section_order_function("Version", version) + section_widths = lasio.writer.get_section_widths("Version", version_section_to_write, version, order_func) + for header_item in version_section_to_write.values(): + mnemonic = header_item.original_mnemonic + # logger.debug('LASFile.write ' + str(header_item)) + order = order_func(mnemonic) + # logger.debug('LASFile.write order = %s' % (order, )) + logger.debug("LASFile.write %s order=%s section_widths=%s" % (header_item, order, section_widths)) + formatter_func = lasio.writer.get_formatter_function(order, **section_widths) + line = formatter_func(header_item) + lines.append(line) + + # ~Well + logger.debug("LASFile.write Well section") + lines.append("~Well ".ljust(header_width, "-")) + order_func = lasio.writer.get_section_order_function("Well", version) + section_widths = lasio.writer.get_section_widths("Well", las.well, version, order_func) + # logger.debug('LASFile.write well section_widths=%s' % section_widths) + for header_item in las.well.values(): + header_item.value = lasio.writer.standardize_value(header_item.value, header_item.unit) + mnemonic = header_item.original_mnemonic + order = order_func(mnemonic) + logger.debug("LASFile.write %s order=%s section_widths=%s" % (header_item, order, section_widths)) + formatter_func = lasio.writer.get_formatter_function(order, **section_widths) + line = formatter_func(header_item) + lines.append(line) + + # ~Curves + logger.debug("LASFile.write Curves section") + lines.append("~Curve Information ".ljust(header_width, "-")) + order_func = lasio.writer.get_section_order_function("Curves", version) + section_widths = lasio.writer.get_section_widths("Curves", las.curves, version, order_func) + for header_item in las.curves: + mnemonic = header_item.original_mnemonic + order = order_func(mnemonic) + formatter_func = lasio.writer.get_formatter_function(order, **section_widths) + line = formatter_func(header_item) + lines.append(line) + + # ~Params + logger.debug("LASFile.write Params section") + parameter_section_header = "~Parameter " if int(version) == 3 else "~Params " + lines.append(parameter_section_header.ljust(header_width, "-")) + order_func = lasio.writer.get_section_order_function("Parameter", version) + section_widths = lasio.writer.get_section_widths("Parameter", las.params, version, order_func) + for header_item in las.params.values(): + header_item.value = lasio.writer.standardize_value(header_item.value, header_item.unit) + mnemonic = header_item.original_mnemonic + order = order_func(mnemonic) + formatter_func = lasio.writer.get_formatter_function(order, **section_widths) + line = formatter_func(header_item) + lines.append(line) + + # ~Other + logger.debug("LASFile.write Other section") + lines.append("~Other ".ljust(header_width, "-")) + lines += las.other.splitlines() + + logger.debug("LASFile.write ASCII section") + + file_object.write("\n".join(lines)) + file_object.write("\n") + line_counter = len(lines) + + # Set empty defaults for nrows and ncols + nrows, ncols = (0, 0) + + # data_arr = np.column_stack([c.data for c in las.curves]) + try: + data_arr = las.data + nrows, ncols = data_arr.shape + logger.debug("Data section shape: {}".format((nrows, ncols))) + except ValueError as err: + logger.debug("Data section is empty") + + logger.debug("len_numeric_field = {}".format(len_numeric_field)) + if len_numeric_field is None: + logger.debug("Calculating len_numeric_field. fmt = {}".format(fmt)) + len_numeric_field = 10 + test_fmt = fmt % np.pi + while len(test_fmt) > (len_numeric_field - 1): + logger.debug("test_fmt = {}".format(test_fmt)) + len_numeric_field += 1 + + def format_data_section_line(n, fmt, l=len_numeric_field, spacing_chars=" "): + try: + if np.isnan(n): + value = str(las.well["NULL"].value) + else: + value = fmt % n + except TypeError: + value = str(n) + + if l != -1: + result = value.rjust(l) + else: + result = value + return spacing_chars + result + + def get_column_fmt(j): + """Get format string for column *j*.""" + if j in column_fmt: + return column_fmt[j] + else: + return fmt + + def get_left_spacing(j): + """Get left-hand-side spacing for column *j*.""" + if j == 0: + left_spacing = lhs_spacer + else: + left_spacing = spacer + return left_spacing + + # Place curve mnemonics in the "~A" line. + if mnemonics_header: + # Calculate width of numeric values from the first line, + header_col_widths = [] + for col_idx in range(ncols): + col_fmt = get_column_fmt(col_idx) + left_spacing = get_left_spacing(col_idx) + data_value = format_data_section_line(data_arr[0, col_idx], col_fmt, spacing_chars=left_spacing) + header_col_widths.append(len(data_value)) + + # Construct mnemonics for header line. + header_values = [] + for j, curve in enumerate(las.curves): + col_width = header_col_widths[j] + if len(curve.mnemonic) > (col_width - 1): + width = len(curve.mnemonic) + 1 + else: + width = col_width + value = curve.mnemonic.rjust(width) + header_values.append(value) + + # Add data section header prefix + data_section_header += " " + if len(header_values): + hv = header_values[0] + for k in range(len(data_section_header)): + if k < len(hv): + if hv[0] == " ": + hv = hv[1:] + header_values = [hv] + header_values[1:] + + file_object.write(data_section_header + "".join(header_values) + "\n") + else: + file_object.write((data_section_header + " ").ljust(header_width, "-") + "\n") + + twrapper = textwrap.TextWrapper(width=data_width) + + for i in range(nrows): + logger.debug("Writing data array row {} of {}".format(i + 1, nrows)) + depth_slice = "" + for j in range(ncols): + col_fmt = get_column_fmt(j) + left_spacing = get_left_spacing(j) + depth_slice += format_data_section_line(data_arr[i, j], col_fmt, spacing_chars=left_spacing) + + if wrap: + lines = twrapper.wrap(depth_slice) + logger.debug("LASFile.write Wrapped %d lines out of %s" % (len(lines), depth_slice)) + else: + lines = [depth_slice] + + for line in lines: + if version_section_to_write.VERS == 1.2 and len(line) > 255: + logger.warning("[v1.2] line #{} has {} chars (>256)".format(line_counter + 1, len(line))) + file_object.write(line + "\n") + line_counter += 1 diff --git a/src/ltrace/ltrace/pore_networks/functions.py b/src/ltrace/ltrace/pore_networks/functions.py index a5722ba..fdadcd7 100644 --- a/src/ltrace/ltrace/pore_networks/functions.py +++ b/src/ltrace/ltrace/pore_networks/functions.py @@ -16,6 +16,7 @@ import porespy from porespy.networks import regions_to_network, snow2 import pandas as pd +import logging from ltrace.slicer_utils import tableNodeToDict, slicer_is_in_developer_mode, dataFrameToTableNode from ltrace.image import optimized_transforms @@ -179,6 +180,7 @@ def set_subresolution_conductance(sub_network, subresolution_function): pore_conductivity_resolved = sub_network["pore.manual_valvatne_conductivity"] pore_phi = sub_network["pore.subresolution_porosity"] pore_pressure = np.array([subresolution_function(p) for p in pore_phi]) + pore_pressure[pore_phi == 1] = np.array([pressure2radius(r) for r in sub_network["pore.diameter"] / 2])[pore_phi == 1] pore_capilar_radius = np.array([pressure2radius(Pc) for Pc in pore_pressure]) sub_network["pore.capilar_radius"] = pore_capilar_radius @@ -191,33 +193,18 @@ def set_subresolution_conductance(sub_network, subresolution_function): pore_conductivity = (1/8) * np.pi * pore_capilar_radius**4 pore_conductivity *= pore_number_of_capilaries - # Throat conductivity - throat_phi = np.ones_like(sub_network["throat.all"], dtype=np.float64) - for throat_index, (left_index, right_index) in enumerate( - sub_network["throat.conns"], - ): - left_unresolved = sub_network["throat.phases"][throat_index][0] == 2 - right_unresolved = sub_network["throat.phases"][throat_index][1] == 2 - - if left_unresolved and not right_unresolved: - throat_phi[throat_index] = pore_phi[left_index] - - elif right_unresolved and not left_unresolved: - throat_phi[throat_index] = pore_phi[right_index] - - elif right_unresolved and left_unresolved: - throat_phi[throat_index] = ( - pore_phi[left_index] - * sub_network["throat.conns_0_length"][throat_index] - + pore_phi[right_index] - * sub_network["throat.conns_1_length"][throat_index] - ) / ( - sub_network["throat.conns_0_length"][throat_index] - + sub_network["throat.conns_1_length"][throat_index] - ) - + throat_phi = sub_network["throat.subresolution_porosity"] throat_pressure = np.array([subresolution_function(p) for p in throat_phi]) + throat_pressure[throat_phi == 1] = np.array([pressure2radius(r) for r in sub_network["throat.diameter"] / 2])[throat_phi == 1] throat_capilar_radius = np.array([pressure2radius(Pc) for Pc in throat_pressure]) + + # Throat diameter debug fix + throat_diameter_temp = np.array(sub_network["throat.diameter"]) + non_zero_throat_diameters = throat_diameter_temp[throat_diameter_temp != 0] + min_throat_diameter = np.min(non_zero_throat_diameters) + throat_diameter_temp[throat_diameter_temp == 0.0] = min_throat_diameter + sub_network["throat.diameter"] = throat_diameter_temp + throat_number_of_capilaries = ( (area_function(sub_network["throat.diameter"]/2) * throat_phi) / area_function(throat_capilar_radius) @@ -297,6 +284,11 @@ def set_subresolution_conductance(sub_network, subresolution_function): sub_network['throat.manual_valvatne_conductance'] = throat_conductance sub_network['throat.number_of_capilaries'] = throat_number_of_capilaries sub_network['pore.number_of_capilaries'] = pore_number_of_capilaries + + sub_network["throat.cross_sectional_area"] = np.pi * sub_network["throat.cap_radius"] ** 2 + sub_network["throat.volume"] = sub_network["throat.total_length"] * sub_network["throat.cross_sectional_area"] + sub_network["pore.volume"] *= sub_network["pore.subresolution_porosity"] + print(os.getcwd()) for element in ("pore.", "throat."): pore_keys = [key for key in sub_network.keys() if key.startswith(element)] @@ -347,6 +339,9 @@ def geo2pnf(geo_pore, subresolution_function, scale_factor=10 ** -3, axis="x"): volume_multiplier = 1 connected_pores, connected_throats = get_connected_geo_network(pore_dict, throat_dict, f"{axis}min", f"{axis}max") + if not any(connected_pores) or not any(connected_throats): + logging.warning("The network is invalid. Does not percolate.") + return None pore_dict, throat_dict = get_sub_geo(pore_dict, throat_dict, connected_pores, connected_throats) pores_with_edge_throats = set() @@ -904,7 +899,7 @@ def porespy_extract(multiphase, watershed, scale, porosity_map=None): if watershed is None: input_array = multiphase - snow_results = snow2(multiphase, porosity_map=porosity_map, voxel_size=scale) + snow_results = snow2(multiphase, porosity_map=porosity_map, voxel_size=scale, parallelization=None) pn_properties = snow_results.network watershed_image = snow_results.regions else: @@ -1048,10 +1043,32 @@ def porespy_extract(multiphase, watershed, scale, porosity_map=None): #rng = np.random.default_rng() #pore_subresolution_porosity = rng.random((pn_properties["pore.all"]).size) - pn_properties["throat.subresolution_porosity"] = ( - pore_subresolution_porosity[pn_properties["throat.conns_0"]] - + pore_subresolution_porosity[pn_properties["throat.conns_1"]] - ) / 2 + throat_phi = np.ones_like(pn_properties["throat.all"], dtype=np.float64) + for throat_index in range(len(pn_properties["throat.all"])): + left_index = pn_properties["throat.conns_0"][throat_index] + right_index = pn_properties["throat.conns_1"][throat_index] + + left_unresolved = pn_properties["throat.phases_0"][throat_index] == 2 + right_unresolved = pn_properties["throat.phases_1"][throat_index] == 2 + + if right_unresolved and left_unresolved: + throat_phi[throat_index] = ( + pore_subresolution_porosity[left_index] + * pn_properties["throat.conns_0_length"][throat_index] + + pore_subresolution_porosity[right_index] + * pn_properties["throat.conns_1_length"][throat_index] + ) / ( + pn_properties["throat.conns_0_length"][throat_index] + + pn_properties["throat.conns_1_length"][throat_index] + ) + + elif left_unresolved and not right_unresolved: + throat_phi[throat_index] = pore_subresolution_porosity[left_index] + + elif right_unresolved and not left_unresolved: + throat_phi[throat_index] = pore_subresolution_porosity[right_index] + + pn_properties["throat.subresolution_porosity"] = throat_phi return pn_properties @@ -1358,15 +1375,18 @@ def visualize( _ = elementIdList.InsertNextId(j) _ = elements.InsertNextCell(elementIdList) - radius = vtk.vtkFloatArray() - for i, j in diameters_list_by_phase[phase]: - radius.InsertTuple1(i, j) + radius = vtk.vtkDoubleArray() + radius.SetNumberOfTuples(len(diameters_list_by_phase[phase])) + radius.SetName("TubeRadius") + for i, dia in diameters_list_by_phase[phase]: + radius.SetTuple1(i, dia) ### Setup VTK filters ### polydata = vtk.vtkPolyData() polydata.SetPoints(coordinates) polydata.SetLines(elements) - polydata.GetPointData().SetScalars(radius) + polydata.GetPointData().AddArray(radius) + polydata.GetPointData().SetActiveScalars("TubeRadius") min_radius = min_diameter_by_phase[phase] / (2 * (phase+1)) # VTK tubes filter uses max_radius as a multiple of minimum radius @@ -1413,5 +1433,7 @@ def visualize( model_node.HardenTransform() slicer.mrmlScene.RemoveNode(transformNode) + return {"pores_nodes": pores_model_nodes, "throats_nodes": throats_model_nodes} + class PoreNetworkExtractorError(RuntimeError): pass diff --git a/src/ltrace/ltrace/pore_networks/generalized_network_extractor.py b/src/ltrace/ltrace/pore_networks/generalized_network_extractor.py index b6be570..eeefc9c 100644 --- a/src/ltrace/ltrace/pore_networks/generalized_network_extractor.py +++ b/src/ltrace/ltrace/pore_networks/generalized_network_extractor.py @@ -43,7 +43,7 @@ def generate_pore_distance_map(pore_label_map): if pore_label_map.shape[0] == 1: pore_distance_map = distance_transform_edt(pore_label_map) else: - pore_distance_map = np.sqrt(pyedt.edt(pore_label_map)) + pore_distance_map = pyedt.edt(pore_label_map) return pore_distance_map diff --git a/src/ltrace/ltrace/pore_networks/simulation_parameters_node.py b/src/ltrace/ltrace/pore_networks/simulation_parameters_node.py index bc3625d..6e87804 100644 --- a/src/ltrace/ltrace/pore_networks/simulation_parameters_node.py +++ b/src/ltrace/ltrace/pore_networks/simulation_parameters_node.py @@ -27,8 +27,10 @@ def parameter_node_to_dict(parameterNode): return parameters_dict -def dict_to_parameter_node(parameter_dict, node_name, parent_node=None): - return dataframe_to_parameter_node(parameters_dict_to_dataframe(parameter_dict), node_name, parent_node) +def dict_to_parameter_node(parameter_dict, node_name, parent_node=None, update_current_node=False): + return dataframe_to_parameter_node( + parameters_dict_to_dataframe(parameter_dict), node_name, parent_node, update_current_node + ) def parameters_dict_to_dataframe(parameter_dict: dict) -> pd.DataFrame: @@ -53,9 +55,13 @@ def parameters_dict_to_dataframe(parameter_dict: dict) -> pd.DataFrame: return df -def dataframe_to_parameter_node(input_values_df, node_name, parent_node=None): +def dataframe_to_parameter_node(input_values_df, node_name, parent_node=None, update_current_node=False): + if update_current_node: + slicer.mrmlScene.RemoveNode(parent_node) + slicer.app.processEvents() + parameterNode = dataFrameToTableNode(input_values_df) - newParameterNodeName = slicer.mrmlScene.GenerateUniqueName(node_name) + newParameterNodeName = slicer.mrmlScene.GenerateUniqueName(node_name) if not update_current_node else node_name parameterNode.SetName(newParameterNodeName) subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) if parent_node: diff --git a/src/ltrace/ltrace/pore_networks/visualization_model.py b/src/ltrace/ltrace/pore_networks/visualization_model.py index d9c1779..6462d1d 100644 --- a/src/ltrace/ltrace/pore_networks/visualization_model.py +++ b/src/ltrace/ltrace/pore_networks/visualization_model.py @@ -1,9 +1,8 @@ -import logging import os import re + import numpy as np import vtk - from vtk.util import numpy_support @@ -183,14 +182,10 @@ def generate_model_variable_scalar(temp_folder, min_saturation_delta=0.005): pressures.append(pressure) previous_array = new_array file = open(filepath, "r") - try: - result = re.search("", file.read()) - data_points.append(float(result.group(1))) - data_cycles.append(float(result.group(2))) - except Exception as error: - logging.debug(f"Error: {error}") - finally: - file.close() + result = re.search("", file.read()) + data_points.append(float(result.group(1))) + data_cycles.append(float(result.group(2))) + file.close() saturation_steps = i diff --git a/src/ltrace/ltrace/readers/microtom/collector.py b/src/ltrace/ltrace/readers/microtom/collector.py index 93da5ef..a7e6be4 100644 --- a/src/ltrace/ltrace/readers/microtom/collector.py +++ b/src/ltrace/ltrace/readers/microtom/collector.py @@ -7,7 +7,6 @@ import re import slicer -import traceback from ltrace.slicer import helpers, data_utils as du @@ -104,12 +103,17 @@ def compile(self, results): except TypeError: self.missing_results.append((sim_output_filepath, "Invalid file format")) - logging.error(f"{repr(e)}\n{traceback.print_exc()}") + import traceback + + traceback.print_exc() except FileNotFoundError as e: self.missing_results.append((sim_output_filepath, "File not found")) except Exception as e: self.missing_results.append((sim_output_filepath, repr(e))) - logging.error(f"{repr(e)}\n{traceback.print_exc()}") + logging.error(repr(e)) + import traceback + + traceback.print_exc() return nodes, ref_node, project_name @@ -189,7 +193,7 @@ def compile(self, results): self.missing_results.append((sim_output_filepath, "File not found")) continue except Exception as e: - logging.error(f"{repr(e)}\n{traceback.print_exc()}") + logging.error(e) continue name = "_".join([v.upper() for v in (prefix, simulator, direction) if v]) + "_Variables" @@ -275,12 +279,17 @@ def compile(self, results): except TypeError: self.missing_results.append((sim_output_filepath, "Invalid file format")) - logging.error(f"Invalid file format.\n{traceback.print_exc()}") + import traceback + + traceback.print_exc() except FileNotFoundError as e: self.missing_results.append((sim_output_filepath, "File not found")) except Exception as e: self.missing_results.append((sim_output_filepath, repr(e))) - logging.error(f"{repr(e)}\n{traceback.print_exc()}") + print("--------------------------------------------------------------------------------------------") + import traceback + + traceback.print_exc() return nodes, ref_node, project_name diff --git a/src/ltrace/ltrace/remote/hosts/utils.py b/src/ltrace/ltrace/remote/hosts/utils.py index d39085d..072d0ee 100644 --- a/src/ltrace/ltrace/remote/hosts/utils.py +++ b/src/ltrace/ltrace/remote/hosts/utils.py @@ -41,7 +41,7 @@ def get_module_from_path(path: Path) -> Union["module", None]: def get_modules() -> List[Path]: modules = [] for file in HOSTS_DIR.rglob("*.py"): - if file.stem == "__init__": + if Path(file).stem == "__init__": continue module = get_module_from_path(file) if module: diff --git a/src/ltrace/ltrace/remote/jobs.py b/src/ltrace/ltrace/remote/jobs.py index dec1c5d..373c28b 100644 --- a/src/ltrace/ltrace/remote/jobs.py +++ b/src/ltrace/ltrace/remote/jobs.py @@ -1,15 +1,16 @@ -import json +from collections import OrderedDict import logging -import traceback +from queue import Queue -from collections import OrderedDict -from ltrace.remote import errors -from ltrace.remote.connections import ConnectionManager, JobExecutor +import json from pathlib import Path -from queue import Queue + from typing import Callable, Dict, List from threading import Lock, Thread +from ltrace.remote import errors +from ltrace.remote.connections import ConnectionManager, JobExecutor + class JobManager: jobs: OrderedDict[str, JobExecutor] = OrderedDict() @@ -36,8 +37,6 @@ def mount(cls, job: JobExecutor): @classmethod def register(cls, key: str, compiler: Callable): - if key in cls.compilers: - raise ValueError(f"Compiler {key} already registered") cls.compilers[key] = compiler @classmethod @@ -70,10 +69,10 @@ def communicate(cls, uid, event, **kwargs): if job and (job.status not in cls.endstates): cls.send(uid, event, **kwargs) except errors.SSHException as e: - logging.info(f"communicate function failed ON CONNECTION: {repr(e)}") + print("communicate function failed ON CONNECTION: ", repr(e)) cls.agenda.put((uid, event)) except Exception as e: - logging.info(f"communicate function failed: {repr(e)}") + print("communicate function failed: ", repr(e)) finally: pass @@ -124,9 +123,16 @@ def set_state( for observer in cls.observers: observer(job, "JOB_MODIFIED") except AttributeError: - logging.info(f"Job not existing anymore, skipping set_state.\nTraceback:\n{traceback.print_exc()}") + logging.info("Job not existing anymore, skipping set_state") + import traceback + + traceback.print_exc() + logging.info("---------------") except Exception as e: - logging.error(f"Traceback:\n{traceback.print_exc()}\nOn jobs.JobManager.set_state = {repr(e)}.") + import traceback + + traceback.print_exc() + logging.error(f"on jobs.JobManager.set_state = {repr(e)}") finally: pass @@ -205,6 +211,7 @@ def resume(cls, job): return False except Exception as e: # TODO return for accounts instead of login + import traceback cls.set_state(job.uid, status="IDLE", traceback={"[ERROR] Unable to connect": traceback.format_exc()}) @@ -232,6 +239,8 @@ def load(cls): f"Error loading previous jobs. Current file has a invalid JSON format.\nDetails: {repr(e)}. File: {jobfile}" ) except Exception as e: + import traceback + logging.error(traceback.format_exc()) @classmethod diff --git a/src/ltrace/ltrace/screenshot/Screenshot.py b/src/ltrace/ltrace/screenshot/Screenshot.py index 5b00590..8761b10 100644 --- a/src/ltrace/ltrace/screenshot/Screenshot.py +++ b/src/ltrace/ltrace/screenshot/Screenshot.py @@ -66,7 +66,9 @@ def _captureSliceView(sliceName, isTransparent, fileName): class ScreenshotWidget(qt.QDialog): + VIEW_OPTION = ["Red", "Green", "Yellow"] THREED_VIEW_OPTION = "3D View" + FONT_ANNOTATION_INITIAL_VALUE = 12 SETTINGS_NAME = "ScreenShotWidget" VIEW_SETTINGS_KEY = "/".join((SETTINGS_NAME, "view")) @@ -82,32 +84,164 @@ def __init__(self, icon): self.iconPath = icon self.setWindowTitle("Screenshot") self.setAttribute(qt.Qt.WA_DeleteOnClose) + self.saved_lines = [] self.setup() def setup(self): + layout = qt.QFormLayout(self) + + # View option self.viewCombobox = qt.QComboBox() options = (self.THREED_VIEW_OPTION,) + slicer.app.layoutManager().sliceViewNames() for option in options: self.viewCombobox.addItem(option) self.viewCombobox.currentText = slicer.app.settings().value(self.VIEW_SETTINGS_KEY, self.THREED_VIEW_OPTION) - self.transparentCheck = qt.QCheckBox() self.transparentCheck.checked = slicer.app.settings().value(self.IS_TRANSPARENT_SETTINGS_KEY, False) == "True" + layout.addRow("View to capture:", self.viewCombobox) + layout.addRow("Transparent background:", self.transparentCheck) + # Annotations options + hbox_line = qt.QHBoxLayout() + line_left = qt.QFrame() + line_left.setFrameShape(qt.QFrame.HLine) + line_left.setFrameShadow(qt.QFrame.Sunken) + hbox_line.addWidget(line_left) + text_label = qt.QLabel(" Add Annotations") + hbox_line.addWidget(text_label) + line_right = qt.QFrame() + line_right.setFrameShape(qt.QFrame.HLine) + line_right.setFrameShadow(qt.QFrame.Sunken) + line = qt.QFrame() + line.setFrameShape(qt.QFrame.HLine) + line.setFrameShadow(qt.QFrame.Sunken) + hbox_line.addWidget(line_right) + layout.addRow(hbox_line) + + self.input = qt.QLineEdit("Click Add") + layout.addRow("Annotation:", self.input) + + hbox = qt.QHBoxLayout() + self.radio_left = qt.QRadioButton("Left") + self.radio_center = qt.QRadioButton("Center") + self.radio_right = qt.QRadioButton("Right") + self.radio_center.setChecked(True) + hbox.addWidget(self.radio_left) + hbox.addWidget(self.radio_center) + hbox.addWidget(self.radio_right) + layout.addRow("Text Position:", hbox) + + self.fontSizeSlider = slicer.qMRMLSliderWidget() + self.fontSizeSlider.maximum = 100 + self.fontSizeSlider.minimum = 12 + self.fontSizeSlider.value = 15 + self.fontSizeSlider.singleStep = 1 + layout.addRow("Font Size:", self.fontSizeSlider) + + textButtons = qt.QHBoxLayout() + addTextButton = qt.QPushButton("Add") + removeTextButton = qt.QPushButton("Remove") + addTextButton.setFixedWidth(100) + removeTextButton.setFixedWidth(100) + textButtons.addWidget(addTextButton) + textButtons.addWidget(removeTextButton) + textButtons.setAlignment(qt.Qt.AlignCenter) + layout.addRow(textButtons) + + layout.addRow(line) + layout.addRow(qt.QFrame()) + + # Save options buttonBox = qt.QDialogButtonBox() buttonBox.addButton(qt.QPushButton("Save as…"), qt.QDialogButtonBox.AcceptRole) buttonBox.addButton(qt.QPushButton("Cancel"), qt.QDialogButtonBox.RejectRole) buttonBox.accepted.connect(self.onSaveAs) - buttonBox.rejected.connect(self.reject) - - layout = qt.QFormLayout(self) - layout.addRow("View to capture:", self.viewCombobox) - layout.addRow("Transparent background:", self.transparentCheck) + buttonBox.rejected.connect(self.onReject) layout.addRow(buttonBox) + self.viewCombobox.currentIndexChanged.connect(lambda font: self.clearAll()) + self.radio_left.clicked.connect(lambda: self.clearAll()) + self.radio_center.clicked.connect(lambda: self.clearAll()) + self.radio_right.clicked.connect(lambda: self.clearAll()) + self.fontSizeSlider.valueChanged.connect(lambda: self.renderText()) + addTextButton.clicked.connect(self.addText) + removeTextButton.clicked.connect(self.removeText) + self.setWindowIcon(qt.QIcon(self.iconPath)) + def addText(self): + input_text = self.input.text + if input_text: + self.saved_lines.append(input_text) + self.input.text = "" + self.renderText() + + def removeText(self): + if self.saved_lines: + del self.saved_lines[-1] + self.renderText() + + def clearAll(self): + textColor = (1.0, 1.0, 1.0) + for viewName in self.VIEW_OPTION: + self.renderInView("", viewName, textColor) + self.renderIn3DView("", textColor) + self.renderText() + + def renderText(self): + textColor = (1.0, 1.0, 1.0) + viewName = self.viewCombobox.currentText + text = self.get_saved_text() + if viewName == self.THREED_VIEW_OPTION: + self.renderIn3DView(text, textColor) + else: + self.renderInView(text, viewName, textColor) + + def renderInView(self, text, viewName, textColor): + lm = slicer.app.layoutManager() + view = lm.sliceWidget(viewName).sliceView() + # Set font + view.cornerAnnotation().ClearAllTexts() + view.cornerAnnotation().SetLinearFontScaleFactor(2) + view.cornerAnnotation().SetNonlinearFontScaleFactor(1) + view.cornerAnnotation().SetMaximumFontSize(int(self.fontSizeSlider.value)) + self.setText(view, text) + # Set color + textProperty = view.cornerAnnotation().GetTextProperty() + textProperty.SetColor(textColor) + view.forceRender() + + def renderIn3DView(self, text, textColor): + view = slicer.app.layoutManager().threeDWidget(0).threeDView() + # Set font + view.cornerAnnotation().SetLinearFontScaleFactor(1.0) + view.cornerAnnotation().SetNonlinearFontScaleFactor(2.0) + view.cornerAnnotation().SetMaximumFontSize(int(self.fontSizeSlider.value)) + self.setText(view, text) + # Set color + textProperty = view.cornerAnnotation().GetTextProperty() + textProperty.SetColor(textColor) + + view.forceRender() + + def setText(self, view, text): + if self.radio_right.checked: + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperRight, text) + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperLeft, "") + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperEdge, "") + if self.radio_center.checked: + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperRight, "") + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperLeft, "") + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperEdge, text) + if self.radio_left.checked: + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperRight, "") + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperLeft, text) + view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperEdge, "") + + def get_saved_text(self): + return "\n".join(self.saved_lines) + def onSaveAs(self): viewName = self.viewCombobox.currentText isTransparent = self.transparentCheck.checked @@ -131,3 +265,14 @@ def onSaveAs(self): _captureSliceView(viewName, isTransparent, fileName) self.accept() + + def onReject(self): + self.saved_lines = [] + self.fontSizeSlider.value = self.FONT_ANNOTATION_INITIAL_VALUE + self.clearAll() + self.reject() + + def closeEvent(self, event): + self.fontSizeSlider.value = self.FONT_ANNOTATION_INITIAL_VALUE + self.saved_lines = [] + self.clearAll() diff --git a/src/ltrace/ltrace/slicer/cache/cache_files.py b/src/ltrace/ltrace/slicer/cache/cache_files.py index 09aec7d..7ea3a01 100644 --- a/src/ltrace/ltrace/slicer/cache/cache_files.py +++ b/src/ltrace/ltrace/slicer/cache/cache_files.py @@ -142,6 +142,7 @@ def add_file(self, file_path: Union[Path, str]) -> bool: if self.is_file_cached(file_path): logging.info(f"File {file_path.as_posix()} is already cached!") + print(f"File {file_path.as_posix()} is already cached!") return False result = "" @@ -149,6 +150,7 @@ def add_file(self, file_path: Union[Path, str]) -> bool: result = shutil.move(file_path, self.__cache_dir / file_path.name) except Exception as error: logging.info(f"Failed to add file to cache directory: {error}") + print(f"Failed to add file to cache directory: {error}") return False return Path(result) == (self.__cache_dir / file_path.name) diff --git a/src/ltrace/ltrace/slicer/cli_utils.py b/src/ltrace/ltrace/slicer/cli_utils.py index 682513a..2729221 100644 --- a/src/ltrace/ltrace/slicer/cli_utils.py +++ b/src/ltrace/ltrace/slicer/cli_utils.py @@ -7,8 +7,6 @@ import slicer.util import mrml import SimpleITK as sitk -from pathlib import Path -from typing import Union, List, Tuple, Callable from vtk.util import numpy_support from ltrace.transforms import clip_to @@ -61,40 +59,26 @@ def _readVectorFrom(volumeFile): return vectorVolumeNode -def readFrom( - volumeFilePath: Union[str, Path], builder: Callable, storageNode: Callable = slicer.vtkMRMLNRRDStorageNode -) -> slicer.vtkMRMLNode: +def readFrom(volumeFile, builder, storageNode=slicer.vtkMRMLNRRDStorageNode): """Reads data from a GeoSlicer's VolumeNode to another Volume Node visible to this CLI use storageNode accordingly. ex. vtkMRMLNRRDStorageNode for volumes and vtkMRMLTableStorageNode for tables. Handles the fact that vtkMRMLNRRDStorageNode cannot read vtkMRMLVectorVolumeNode directly. """ - if isinstance(volumeFilePath, Path): - volumeFilePath = volumeFilePath.as_posix() - if builder == mrml.vtkMRMLVectorVolumeNode: - return _readVectorFrom(volumeFilePath) + return _readVectorFrom(volumeFile) sn = storageNode() - sn.SetFileName(volumeFilePath) + sn.SetFileName(volumeFile) nodeIn = builder() - sn.ReadData(nodeIn) # read data from volumeFilePath into nodeIn + sn.ReadData(nodeIn) # read data from volumeFile into nodeIn return nodeIn -def writeDataInto( - volumeFilePath: Union[str, Path], - dataVoxelArray: np.ndarray, - builder: Callable, - reference: slicer.vtkMRMLNode = None, - spacing: Union[List, Tuple, np.ndarray] = None, -) -> None: +def writeDataInto(volumeFile, dataVoxelArray, builder, reference=None, spacing=None): """Writes data from CLI's Volume Node into GeoSlicer Volume Node""" - if isinstance(volumeFilePath, Path): - volumeFilePath = volumeFilePath.as_posix() - sn_out = slicer.vtkMRMLNRRDStorageNode() - sn_out.SetFileName(volumeFilePath) + sn_out.SetFileName(volumeFile) nodeOut = builder() if reference: @@ -125,8 +109,5 @@ def writeDataInto( sn_out.WriteData(nodeOut) -def writeToTable(df, tableFilePath: Union[Path, str], na_rep: str = "") -> None: - if isinstance(tableFilePath, Path): - tableFilePath = tableFilePath.as_posix() - - df.to_csv(tableFilePath, sep="\t", header=True, index=False, na_rep=na_rep) +def writeToTable(df, tableID, na_rep=""): + df.to_csv(tableID, sep="\t", header=True, index=False, na_rep=na_rep) diff --git a/src/ltrace/ltrace/slicer/color_map_customizer.py b/src/ltrace/ltrace/slicer/color_map_customizer.py index df5213a..d3b2ad8 100644 --- a/src/ltrace/ltrace/slicer/color_map_customizer.py +++ b/src/ltrace/ltrace/slicer/color_map_customizer.py @@ -78,7 +78,8 @@ def add_color_map(cmap): ## not working for every colormap on matplotlib ## only for the ones with 256 colors. c = cmapToColormap(cmap, 256) - n = slicer.vtkMRMLColorTableNode() + n = slicer.mrmlScene.CreateNodeByClass(slicer.vtkMRMLColorTableNode.__name__) + n.UnRegister(None) n.SaveWithSceneOff() n.SetName(cmap.name) lut = vtk.vtkLookupTable() diff --git a/src/ltrace/ltrace/slicer/custom_export_to_file.py b/src/ltrace/ltrace/slicer/custom_export_to_file.py new file mode 100644 index 0000000..60a0a2a --- /dev/null +++ b/src/ltrace/ltrace/slicer/custom_export_to_file.py @@ -0,0 +1,90 @@ +import slicer +import qt +from ltrace.slicer.helpers import getCurrentEnvironment +from ltrace.slicer.node_attributes import NodeEnvironment + +Env = NodeEnvironment + + +def _select_tab(tab_widget, label): + for i in range(tab_widget.count): + if tab_widget.tabText(i) == label: + tab_widget.setCurrentIndex(i) + return tab_widget.widget(i) + + +def _detect_node_env(node, current_env): + if node is None: + return None + if node.IsA("vtkMRMLTableNode") or node.IsA("vtkMRMLSegmentationNode"): + if current_env in [Env.CORE, Env.MICRO_CT, Env.IMAGE_LOG, Env.THIN_SECTION]: + return current_env + if node.IsA("vtkMRMLVectorVolumeNode"): + return Env.THIN_SECTION + if node.IsA("vtkMRMLScalarVolumeNode"): + array = slicer.util.arrayFromVolume(node) + if array.shape[1] == 1: + return Env.IMAGE_LOG + if current_env == Env.CORE: + return Env.CORE + return Env.MICRO_CT + return None + + +def _export_node_as(selected_item_id, env): + if env is None: + slicer.util.warningDisplay( + "Can't export selection. Make sure you have selected a single image, or try using the 'Data > Export' tab of your environment." + ) + return + select_module = slicer.util.mainWindow().moduleSelector().selectModule + if env == Env.THIN_SECTION: + select_module("ThinSectionEnv") + widget = slicer.modules.ThinSectionEnvWidget + + data_widget = _select_tab(widget.mainTab, "Data") + export_widget = _select_tab(data_widget, "Export").self() + + export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id) + return + if env == Env.IMAGE_LOG: + select_module("ImageLogEnv") + widget = slicer.modules.ImageLogEnvWidget + + data_widget = _select_tab(widget.mainTab, "Data") + export_widget = _select_tab(data_widget, "Export").self() + export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id) + return + if env == Env.CORE: + select_module("CoreEnv") + widget = slicer.modules.CoreEnvWidget + + data_widget = _select_tab(widget.mainTab, "Data") + export_widget = _select_tab(data_widget, "Export").self() + + export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id) + return + if env == Env.MICRO_CT: + select_module("MicroCTEnv") + widget = slicer.modules.MicroCTEnvWidget + + data_widget = _select_tab(widget.mainTab, "Data") + export_widget = _select_tab(data_widget, "Export").self() + return + + +def _export_selected_node(): + sh = slicer.mrmlScene.GetSubjectHierarchyNode() + plugin_handler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() + selected_item_id = plugin_handler.currentItem() + node = sh.GetItemDataNode(selected_item_id) + detected_env = _detect_node_env(node, getCurrentEnvironment()) + _export_node_as(selected_item_id, detected_env) + + +def customize_export_to_file(): + plugin_handler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() + export_plugin = plugin_handler.pluginByName("Export") + export_action = export_plugin.findChild(qt.QAction) + export_action.triggered.disconnect() + export_action.triggered.connect(_export_selected_node) diff --git a/src/ltrace/ltrace/slicer/data_utils.py b/src/ltrace/ltrace/slicer/data_utils.py index a569fb2..9b80fb0 100644 --- a/src/ltrace/ltrace/slicer/data_utils.py +++ b/src/ltrace/ltrace/slicer/data_utils.py @@ -1,5 +1,16 @@ import numpy as np -import pandas as pd + +try: + # Suppress "lzma compression not available" UserWarning when loading pandas + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=UserWarning) + import pandas as pd +except ImportError: + raise ImportError( + "Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`" + ) import vtk, slicer @@ -47,3 +58,38 @@ def is_bool(value): tableNode.Modified() tableNode.EndModify(tableWasModified) return tableNode + + +def tableNodeToDataFrame(tableNode): + """Optimized version from slicer.util.dataframeFromTable + + Convert table node content to pandas dataframe. + + Table content is copied. Therefore, changes in table node do not affect the dataframe, + and dataframe changes do not affect the original table node. + """ + + if tableNode is None: + return pd.DataFrame() + + vtable = tableNode.GetTable() + data = [] + columns = [] + for columnIndex in range(vtable.GetNumberOfColumns()): + vcolumn = vtable.GetColumn(columnIndex) + numberOfComponents = vcolumn.GetNumberOfComponents() + column_name = vcolumn.GetName() + columns.append(column_name) + + if numberOfComponents == 1: + column_data = [vcolumn.GetValue(rowIndex) for rowIndex in range(vcolumn.GetNumberOfValues())] + else: + column_data = [] + for rowIndex in range(vcolumn.GetNumberOfTuples()): + item = [vcolumn.GetValue(rowIndex, componentIndex) for componentIndex in range(numberOfComponents)] + column_data.append(tuple(item)) + data.append(column_data) + + dataframe = pd.DataFrame(zip(*data), columns=columns) + + return dataframe diff --git a/src/ltrace/ltrace/slicer/equations/schema.py b/src/ltrace/ltrace/slicer/equations/schema.py deleted file mode 100644 index 9d10237..0000000 --- a/src/ltrace/ltrace/slicer/equations/schema.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -import jsonschema - -from typing import Union, Dict - -from ltrace.slicer.equations.line_equation import LineEquation -from ltrace.slicer.equations.timur_coates_equation import TimurCoatesEquation - -LINE_EQUATION_SCHEMA = { - "title": "Line Equation", - "type": "object", - "required": ["m", "b", "x_min", "x_max"], - "properties": { - "m": {"type": "object"}, - "b": {"type": "object"}, - "x_min": { - "type": "object", - }, - "x_max": { - "type": "object", - }, - }, -} - -TIMUR_COATES_EQUATION_SCHEMA = { - "title": "Timur Coates Equation", - "type": "object", - "required": ["A", "B", "C", "x_min", "x_max", "bins"], - "properties": { - "A": { - "type": "object", - }, - "B": { - "type": "object", - }, - "C": { - "type": "object", - }, - "x_min": { - "type": "object", - }, - "x_max": { - "type": "object", - }, - "bins": { - "type": "object", - }, - }, -} - - -def validateSchema(data: Union[Dict, str]): - if isinstance(data, str): - data = json.loads(data) - - _type = data.get("Fitting equation")["0"] - if not _type: - raise ValueError("Missing 'Fitting equation' property") - - if _type == LineEquation.NAME: - jsonschema.validate(instance=data, schema=LINE_EQUATION_SCHEMA) - elif _type == TimurCoatesEquation.NAME: - jsonschema.validate(instance=data, schema=TIMUR_COATES_EQUATION_SCHEMA) - else: - raise ValueError("Invalid 'Fitting equation' property") diff --git a/src/ltrace/ltrace/slicer/graph_data.py b/src/ltrace/ltrace/slicer/graph_data.py index 2daeda1..752170c 100644 --- a/src/ltrace/ltrace/slicer/graph_data.py +++ b/src/ltrace/ltrace/slicer/graph_data.py @@ -5,10 +5,14 @@ from pandas.api.types import is_numeric_dtype from ltrace.slicer.node_attributes import TableDataOrientation -from ltrace.algorithms.measurements import CLASS_LABEL_SUFFIX, get_pore_size_class_label_field, PORE_SIZE_CATEGORIES +from ltrace.algorithms.measurements import ( + CLASS_LABEL_SUFFIX, + get_pore_size_class_label_field, + PORE_SIZE_CATEGORIES, +) from ltrace.slicer.helpers import tryGetNode from pyqtgraph import QtCore -from slicer.util import dataframeFromTable +from ltrace.slicer import data_utils as dutils TEXT_SYMBOLS = { "● Circle": "o", @@ -50,7 +54,15 @@ class GraphStyle(QtCore.QObject): signalStyleChanged = QtCore.Signal() def __init__( - self, plot_type=None, color=None, symbol=None, size=None, line_style=None, line_size=None, *args, **kwargs + self, + plot_type=None, + color=None, + symbol=None, + size=None, + line_style=None, + line_size=None, + *args, + **kwargs, ): super().__init__(*args, **kwargs) default_color = 211, 47, 47 @@ -187,7 +199,16 @@ class GraphData(QtCore.QObject): signalRemoved = QtCore.Signal() signalStyleChanged = QtCore.Signal() - def __init__(self, parent, plot_type=None, color=None, symbol=None, size=None, *args, **kwargs): + def __init__( + self, + parent, + plot_type=None, + color=None, + symbol=None, + size=None, + *args, + **kwargs, + ): super().__init__(parent) self._data = None self._name = "" @@ -260,9 +281,10 @@ def _handleKnownDataSpecifics(self, df: pd.DataFrame) -> pd.DataFrame: """ if "pore_size_class" in df.columns: # This is a inspector result - TODO make a attribute inside the node - labelColumnName, newLabelColumnData = GraphData.handleLegacyPoreSizeClassVersions( - df.columns.to_list(), df["pore_size_class"] - ) + ( + labelColumnName, + newLabelColumnData, + ) = GraphData.handleLegacyPoreSizeClassVersions(df.columns.to_list(), df["pore_size_class"]) if newLabelColumnData is not None: df[labelColumnName] = newLabelColumnData @@ -280,7 +302,9 @@ def handleLegacyPoreSizeClassVersions(columns, columnData): except ValueError: labelColumnName = f"pore_size_class[label]" newLabelColumnData = columnData.replace( - PORE_SIZE_CATEGORIES, np.arange(0, len(PORE_SIZE_CATEGORIES), 1), inplace=False + PORE_SIZE_CATEGORIES, + np.arange(0, len(PORE_SIZE_CATEGORIES), 1), + inplace=False, ) return labelColumnName, newLabelColumnData @@ -294,7 +318,15 @@ class NodeGraphData(GraphData): """ def __init__( - self, parent, dataNode: slicer.vtkMRMLNode, plot_type=None, color=None, symbol=None, size=None, *args, **kwargs + self, + parent, + dataNode: slicer.vtkMRMLNode, + plot_type=None, + color=None, + symbol=None, + size=None, + *args, + **kwargs, ): super().__init__(parent, plot_type, color, symbol, size, *args, **kwargs) self.__nodeId = None @@ -361,7 +393,7 @@ def __parseTableData(self, dataNode: slicer.vtkMRMLTableNode): if orientation is None: orientation = str(TableDataOrientation.COLUMN.value) - df = dataframeFromTable(dataNode) + df = dutils.tableNodeToDataFrame(dataNode) if orientation == str(TableDataOrientation.ROW.value): df = self.transposeDataframe(df) @@ -417,6 +449,16 @@ def __tryCastColumnsToNumeric(self, df: pd.DataFrame) -> pd.DataFrame: class DataFrameGraphData(GraphData): - def __init__(self, parent, dataFrame, plot_type=None, color=None, symbol=None, size=None, *args, **kwargs): + def __init__( + self, + parent, + dataFrame, + plot_type=None, + color=None, + symbol=None, + size=None, + *args, + **kwargs, + ): super().__init__(parent, plot_type, color, symbol, size, *args, **kwargs) self._data = dataFrame diff --git a/src/ltrace/ltrace/slicer/helpers.py b/src/ltrace/ltrace/slicer/helpers.py index 5c96c9f..70e7516 100644 --- a/src/ltrace/ltrace/slicer/helpers.py +++ b/src/ltrace/ltrace/slicer/helpers.py @@ -22,10 +22,16 @@ from ltrace.slicer.node_attributes import ( NodeEnvironment, NodeTemporarity, + LosslessAttribute, ) +from ltrace.wrappers import timeit + from pathlib import Path from skimage.segmentation import relabel_sequential -from typing import List, Tuple, Union +from typing import Dict, List, Tuple, Union + +from ltrace.slicer import data_utils as dutils + """ Type references: @@ -270,19 +276,19 @@ def getCurrentEnvironment(): def in_image_log_environment(): - return getCurrentEnvironment() == NodeEnvironment.IMAGE_LOG.value + return getCurrentEnvironment() == NodeEnvironment.IMAGE_LOG def in_micro_ct_environment(): - return getCurrentEnvironment() == NodeEnvironment.MICRO_CT.value + return getCurrentEnvironment() == NodeEnvironment.MICRO_CT def in_core_environment(): - return getCurrentEnvironment() == NodeEnvironment.CORE.value + return getCurrentEnvironment() == NodeEnvironment.CORE def in_thin_section_environment(): - return getCurrentEnvironment() == NodeEnvironment.THIN_SECTION.value + return getCurrentEnvironment() == NodeEnvironment.THIN_SECTION def moveNodeTo(dirId, node, dirTree=None): @@ -312,7 +318,7 @@ def createNode(cls, name, environment=None, hidden=True, content=None, display=N if environment is not None: node.SetAttribute(NodeEnvironment.name(), environment.value) - if display is None: + if display is None and hasattr(node, "CreateDefaultDisplayNodes"): node.CreateDefaultDisplayNodes() elif display: node.SetAndObserveDisplayNodeID(display) @@ -1488,6 +1494,120 @@ def arrayFromSegmentBinaryLabelmap(segmentationNode, segmentId, referenceVolumeN return narray +# Copied from ImageLogCSV.py - TODO MUSA-89 +def arrayPartsFromNode(node: slicer.vtkMRMLNode) -> tuple[np.ndarray, np.ndarray]: + + mmToM = 0.001 + if isinstance(node, slicer.vtkMRMLScalarVolumeNode): + values = slicer.util.arrayFromVolume(node).copy().squeeze() + if values.ndim != 2: + raise ValueError(f"Node has dimension {values.ndim}, expected 2.") + + bounds = [0] * 6 + node.GetBounds(bounds) + ymax = -bounds[4] * mmToM + ymin = -bounds[5] * mmToM + spacing = node.GetSpacing()[2] * mmToM + depthColumn = np.arange(ymin, ymax - spacing / 2, spacing) + + ijkToRas = np.zeros([3, 3]) + node.GetIJKToRASDirections(ijkToRas) + if ijkToRas[0][0] > 0: + values = np.flip(values, axis=0) + if ijkToRas[1][1] > 0: + values = np.flip(values, axis=1) + if ijkToRas[2][2] > 0: + values = np.flip(values, axis=2) + elif isinstance(node, slicer.vtkMRMLTableNode): + if node.GetAttribute("table_type") == "histogram_in_depth": + df = dutils.tableNodeToDataFrame(node) # using ltrace's version, not slicer.utils + df_columns = df.columns + depthColumn = df[df_columns[0]].to_numpy() * mmToM + values = df[df_columns[1:]].to_numpy() + else: + values = slicer.util.arrayFromTableColumn(node, node.GetColumnName(1)) + depthColumn = slicer.util.arrayFromTableColumn(node, node.GetColumnName(0)) * mmToM + if depthColumn[0] > depthColumn[-1]: + depthColumn = np.flipud(depthColumn) + values = np.flipud(values) + + return depthColumn, values + + +def redactAnonymize( + node: slicer.vtkMRMLNode, + messDepths: bool, + messData: bool, + newName="DummyName", + newWellName="DummyWell", +): + """Anonymizes data (and optionally edits numerical values) to meet NDA criteria". + + Well name is replaced, node name is replaced, depths can be offset by a random value. And (not working yet) numerical data can be shuffled. + No noise is added to the data so to keep the standard null entries and to preserve the numerical range. + """ + + raise NotImplementedError("redactAnonymize not fully implemented.") + + # messData not working yet + if messData: + messData = False + + node.SetAttribute("WellName", newWellName) + + if newWellName: + node.SetName(f"{newWellName}_{newName}") + else: + node.SetName(f"{newName}") + + if isinstance(node, slicer.vtkMRMLTableNode): + df = None # dutils.tableNodeToDataFrame(node) + df_inner = df.values + depths = df_inner[0:, 0] + dfTo = None # dutils.tableNodeToDataFrame(node) + if messDepths: + offset = 100000.0 + (np.random.random_sample() * 200000.0) + depths += offset + dfTo.iloc[0:, 0] = pd.Series(depths) + dutils.dataFrameToTableNode(dfTo, node) + if messData: # not working yet + values = df_inner[0:, 1:] + values = np.squeeze(values) + np.random.shuffle(values) + values = values.transpose() + np.random.shuffle(values) + values = values.transpose() + dfTo.iloc[0:, 1] = pd.Series(values) # doesn't work for multidimensional arrays + dutils.dataFrameToTableNode(dfTo, node) + else: + values = [] + spacing = [] + origin = [] + if isinstance(node, slicer.vtkMRMLSegmentationNode): + values, spacing, origin = arrayFromVisibleSegmentsBinaryLabelmap(node) + else: + values = slicer.util.arrayFromVolume(node) + origin = node.GetOrigin() + + if messDepths: + new_origin = [ + origin[0], + origin[1], + origin[2] - 100000.0 - (np.random.random_sample() * 200000.0), + ] + else: + new_origin = origin + node.SetOrigin(new_origin) + + if messData: + values = np.squeeze(values) + np.random.shuffle(values) + values = values.transpose() + np.random.shuffle(values) + values = values.transpose() + slicer.util.updateVolumeFromArray(node, values) + + def themeIsDark(): import qt @@ -1611,11 +1731,11 @@ def export_las_from_histogram_in_depth_data(df: pd.DataFrame, file_path: str): df_array = df_array[df_array[:, 0].argsort()] # add depth as double to first curve - lf.add_curve("DEPT", df_array[:-1, 0].astype(np.double), unit="m") + lf.append_curve("DEPT", df_array[:-1, 0].astype(np.double), unit="m") # add remaining data as pore size distribution i for i in range(df_array.shape[1] - 1): - lf.add_curve("DTP" + str(i), df_array[:-1, i + 1]) + lf.append_curve("DTP" + str(i), df_array[:-1, i + 1]) lf.well.append(lasio.HeaderItem(mnemonic="BIN0", value=df_array[-1, 1], descr="VALOR INICIAL DOS BINS")) lf.well.append(lasio.HeaderItem(mnemonic="BINN", value=df_array[-1, -1], descr="VALOR FINAL DOS BINS")) @@ -1716,12 +1836,6 @@ def getVolumeVisibilityIn3D(volumeNode): return renderingNode.GetVisibility() if renderingNode else False -def setSlicesVisibilityIn3D(visible): - for sliceViewLabel in ["Red", "Yellow", "Green"]: - sliceView = slicer.app.layoutManager().sliceWidget(sliceViewLabel).sliceView() - sliceView.mrmlSliceNode().SetSliceVisible(visible) - - def get_memory_usage(mode="bytes"): """Get the Geoslicer memory usage as percent value diff --git a/src/ltrace/ltrace/slicer/lazy/protocols/base.py b/src/ltrace/ltrace/slicer/lazy/protocols/base.py index 0676bf8..a9b563e 100644 --- a/src/ltrace/ltrace/slicer/lazy/protocols/base.py +++ b/src/ltrace/ltrace/slicer/lazy/protocols/base.py @@ -24,4 +24,4 @@ def load(self, *args, **kwargs) -> "xr.Dataset": @staticmethod def host(*args, **kwargs) -> Host: - return Host() + return Host("", "") diff --git a/src/ltrace/ltrace/slicer/node_observer.py b/src/ltrace/ltrace/slicer/node_observer.py index a662fd9..f90c529 100644 --- a/src/ltrace/ltrace/slicer/node_observer.py +++ b/src/ltrace/ltrace/slicer/node_observer.py @@ -48,7 +48,6 @@ def __on_node_removed(self, caller, event, node): """Handles node's removal.""" if node is None or node.GetID() != self.__nodeId: return - try: self.removedSignal.emit(self, node) self.clear() @@ -59,6 +58,14 @@ def __on_node_removed(self, caller, event, node): def clear(self): """Clears current object's data.""" + try: + self.children() + self.modifiedSignal.disconnect() + self.removedSignal.disconnect() + except ValueError: + # Object has been deleted + pass + for obj, tag in self.__observerHandlers: obj.RemoveObserver(tag) diff --git a/src/ltrace/ltrace/slicer/project_manager.py b/src/ltrace/ltrace/slicer/project_manager.py index b237211..c36cfbd 100644 --- a/src/ltrace/ltrace/slicer/project_manager.py +++ b/src/ltrace/ltrace/slicer/project_manager.py @@ -60,6 +60,45 @@ def __on_node_modified(self, caller, event): self.__last_visibility = caller.GetVisibility() +def BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node) -> str: + """Handle the extra copy at the end of the file name. + This is a bug fix for the method AddDefaultStorageNode, that probably generate the wrong name internally + (TODO check this). + + Args: + filename (str): the file name to be fixed. + + Returns: + str: the fixed file name. + """ + filename = storage_node.GetFileName() + if not filename: + return "" + + patters = [r" Copy", r".nrrd Copy.nrrd"] + func = lambda fn: max([len(p) if fn.endswith(p) else 0 for p in patters]) + + found = func(filename) + + if found > 0: + filepath = Path(filename[:-found]) + filename = str(filepath.with_name(f"{filepath.stem} Copy{filepath.suffix}")) + storage_node.SetFileName(filename) + + return filename + + +def generate_unique_node_name(name: str, dirpath: Path): + count = 0 + for file in dirpath.iterdir(): + if file.is_file() and file.name.startswith(name): + count += 1 + + unique_name = f"{name} ({count})" if count > 0 else name + + return unique_name + + @singleton class ProjectManager(qt.QObject): """Class to handle the 'project concept' from Geoslicer. @@ -154,8 +193,11 @@ def save_as(self, scene_path, *args, **kwargs): return status - def load(self, project_file_path, internal_call=False): + def load(self, project_file_path, internal_call=False) -> None: """Handle custom load scene operation.""" + if isinstance(project_file_path, Path): + project_file_path = project_file_path.as_posix() + if project_file_path == slicer.mrmlScene.GetURL(): return @@ -164,11 +206,15 @@ def load(self, project_file_path, internal_call=False): self.__clear_node_observers() # Close scene before load a new one - slicer.mrmlScene.Clear(0) + self.close() slicer.util.loadScene(project_file_path) self.__set_project_modified(False) + def close(self) -> None: + """Wrapper method to close the project.""" + slicer.mrmlScene.Clear(0) + def setup(self): """Initialize project's event handlers""" self.end_close_scene_observer_handler = slicer.mrmlScene.AddObserver( @@ -215,13 +261,18 @@ def __on_end_load_scene(self, *args, **kwargs): viewNode.SetAxisLabelsVisible(False) # Without this the Image Log segmenter doesn't correctly restore the selected segmentation node - image_log_data_logic = slicer.modules.imagelogdata.widgetRepresentation().self().logic - image_log_data_logic.imageLogSegmenterWidget.self().initializeSavedNodes() + try: + image_log_data_logic = slicer.modules.imagelogdata.widgetRepresentation().self().logic + image_log_data_logic.imageLogSegmenterWidget.self().initializeSavedNodes() - # Image Log number of views restoration - if slicer.app.layoutManager().layout >= 15000: - image_log_data_logic.configurationsNode = None - image_log_data_logic.loadConfiguration() + # Image Log number of views restoration + if slicer.app.layoutManager().layout >= 15000: + image_log_data_logic.configurationsNode = None + image_log_data_logic.loadConfiguration() + + except ValueError: + # Widget has been deleted after test + pass self.__clear_mask_settings_on_all_segment_editors() @@ -232,6 +283,18 @@ def __on_scene_modified(self, *args, **kwargs): @vtk.calldata_type(vtk.VTK_OBJECT) def __on_node_added(self, caller, eventId, callData): """Handle slicer' node added to scene event.""" + if issubclass( + type(callData), + ( + slicer.vtkMRMLModelNode, + slicer.vtkMRMLColorTableNode, + slicer.vtkMRMLSubjectHierarchyNode, + slicer.vtkMRMLTransformNode, + slicer.vtkMRMLSliceDisplayNode, + ), + ): + return + observer = NodeObserver(node=callData, parent=self) observer.modifiedSignal.connect(self.__on_scene_modified) observer.removedSignal.connect(self.__on_observed_node_removed) @@ -244,8 +307,15 @@ def onVolumeModified(node_observer: NodeObserver, node: slicer.vtkMRMLNode) -> N if node is None: return - # Wait for the volume to load - qt.QTimer.singleShot(0, lambda: self.__on_volume_modified(node)) + timer = qt.QTimer() + + def onTimeout(): + self.__on_volume_modified(node) + timer.timeout.disconnect(onTimeout) + + timer.setSingleShot(True) + timer.timeout.connect(onTimeout) + timer.start(0) observer.modifiedSignal.connect(onVolumeModified) elif isinstance(callData, slicer.vtkMRMLVolumeRenderingDisplayNode): @@ -262,11 +332,13 @@ def __on_observed_node_removed(self, node_observer: NodeObserver, node: slicer.v return self.__node_observers.remove(node_observer) + del node_observer def __clear_node_observers(self): """Clear the node observer's list and remove the observer handlers from each one.""" - for node_observer in self.__node_observers: + for node_observer in self.__node_observers[:]: node_observer.clear() + del node_observer self.__node_observers.clear() @@ -451,6 +523,21 @@ def __save_nodes(self, *args, **kwargs): return status + def __get_localized_storage_node(self, node, local_storage_dir): + storage_node = node.GetStorageNode() + + # All files should be stored in the Data directory. + # If the node's file name is in another directory, create a new default storage node. + if storage_node and storage_node.GetFileName(): + file_path = Path(storage_node.GetFileName()).resolve() + if file_path.parent != local_storage_dir: + slicer.mrmlScene.RemoveNode(storage_node) + node.AddDefaultStorageNode() + else: + node.AddDefaultStorageNode() + + return node.GetStorageNode() + def __handle_storable_node(self, node, *args, **kwargs): """Function that checks if storable node has a valid storage node or if it could be created. In case of creating a new storage node, it will define its filename. @@ -464,43 +551,21 @@ def __handle_storable_node(self, node, *args, **kwargs): Returns: bool: True if node has a valid storage node, otherwise returns False. """ - data_folder = Path(slicer.mrmlScene.GetRootDirectory()) / "Data" - if not hasattr(node, "GetStorageNode"): return False - storage_node = node.GetStorageNode() - - # All files should be stored in the Data directory. - # If the node's file name is in another directory, create a new default storage node. - if storage_node and storage_node.GetFileName(): - file_path = Path(storage_node.GetFileName()).resolve() - if file_path.parent != data_folder: - slicer.mrmlScene.RemoveNode(storage_node) + data_folder = Path(slicer.mrmlScene.GetRootDirectory()) / "Data" - storage_node = node.GetStorageNode() - if storage_node is None: - if not node.AddDefaultStorageNode(): - # If storable node doesn't have a storage node and isn't possible to create one, - # it means that scene will handle its saving. - # ref: https://github.com/Slicer/Slicer/blob/78f426ec6abc5ec6b0513542556b2016c1f54852/Base/QTGUI/qSlicerSaveDataDialog.cxx#L528 - return False - - # Retrieve storage node again, - # in case of method 'AddDefaultStorageNode' call happened - storage_node = node.GetStorageNode() - if storage_node is None: + storage_node = self.__get_localized_storage_node(node, data_folder) + if not storage_node: return False - file_path = storage_node.GetFileName() - if file_path == "" or file_path is None or is_valid_filename(os.path.basename(file_path)) is False: - # Define node's filepath + filepath = Path(BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node)) - file_path = self.__create_node_file_name(str(data_folder), node) - if file_path is None: - return False - - storage_node.SetFileName(file_path) + if not (filepath and is_valid_filename(filepath.name)): + filename = generate_unique_node_name(node.GetName(), data_folder) + ext = storage_node.GetDefaultWriteFileExtension() + storage_node.SetFileName(str(data_folder / filename) + f".{ext}") # Define compression mode properties = DEFAULT_PROPERTIES.copy() @@ -513,43 +578,6 @@ def __handle_storable_node(self, node, *args, **kwargs): return True - def __create_node_file_name(self, file_folder, node): - """Handles node's filename creation. - - Args: - file_folder (str): the folder where file will be created. - node (vtk.vtkMRMLStorableNode): the node object. - - Returns: - str/None: the node's filename. None if node's doesn't have a storage node. - """ - storage_node = node.GetStorageNode() - if storage_node is None: - return None - - file_extension = storage_node.GetDefaultWriteFileExtension() - index = 0 - while True: - if index == 0: - file_name = f"{node.GetName()}.{file_extension}" - else: - file_name = f"{node.GetName()} ({index}).{file_extension}" - - file_path = os.path.join(file_folder, file_name) - file_path = sanitize_filepath(file_path=file_path, platform="auto") - - if os.path.exists(file_path) is False: - break - - index += 1 - if index >= 1000: - logging.error( - "A problem has occured during node's filename creation. Stopping process to avoid infinite loop." - ) - break - - return file_path - def __save_scene(self, project_url, *args, **kwargs): """Handle the nodes saving process. diff --git a/src/ltrace/ltrace/slicer/side_by_side_image_layout.py b/src/ltrace/ltrace/slicer/side_by_side_image_layout.py index 5f5241d..566fe23 100644 --- a/src/ltrace/ltrace/slicer/side_by_side_image_layout.py +++ b/src/ltrace/ltrace/slicer/side_by_side_image_layout.py @@ -30,7 +30,7 @@ def __init__(self): allowedInputNodes=["vtkMRMLSegmentationNode"], ) customSegmentSelector.setObjectName("CustomSegmentSelector") - customSegmentSelector.onMainSelected = partial(self.onSegmentationChanged, i) + customSegmentSelector.onMainSelectedSignal.connect(partial(self.onSegmentationChanged, i)) customSegmentSelector.segmentSelectionChanged.connect( lambda segments, i=i: self.onSegmentSelectionChanged(i, segments) @@ -141,3 +141,55 @@ def onSegmentSelectionChanged(self, sliceIndex, selectedSegments): for segmentIndex in selectedSegments: segmentId = segmentation.GetNthSegmentID(segmentIndex) displayNode.SetSegmentVisibility(segmentId, True) + + +POSITION_FLAG = slicer.vtkMRMLSliceNode.SliceToRASFlag +ZOOM_FLAG = slicer.vtkMRMLSliceNode.FieldOfViewFlag +FIT_VOLUME_FLAG = slicer.vtkMRMLSliceNode.ResetFieldOfViewFlag +SLICE_OFFSET_FLAG = slicer.vtkMRMLSliceNode.XYZOriginFlag + +FLAG_LIST = [POSITION_FLAG, ZOOM_FLAG, FIT_VOLUME_FLAG, SLICE_OFFSET_FLAG] +ALL_FLAGS = POSITION_FLAG | ZOOM_FLAG | SLICE_OFFSET_FLAG + + +def _sync(sliceNode): + """Sync position, zoom, slice offset of other slice views with this slice view.""" + sliceNode.SetInteracting(1) + sliceNode.SetInteractionFlags(ALL_FLAGS) + sliceNode.Modified() + sliceNode.SetInteractionFlags(0) + sliceNode.SetInteracting(0) + + +def _onSliceNodeModified(caller, event): + interaction = caller.GetInteractionFlags() + if interaction in FLAG_LIST and caller.GetInteracting(): + _sync(caller) + + +def _onCompositeNodeModified(sliceNode, caller, event): + if caller.GetInteracting(): + return + if caller.GetInteractionFlags(): + return + if not caller.GetLinkedControl(): + return + _sync(sliceNode) + + +def setupViews(viewName1, viewName2): + sliceWidget1 = slicer.app.layoutManager().sliceWidget(viewName1) + sliceWidget2 = slicer.app.layoutManager().sliceWidget(viewName2) + sliceNode1 = sliceWidget1.sliceLogic().GetSliceNode() + sliceNode2 = sliceWidget2.sliceLogic().GetSliceNode() + + sliceNode1.AddObserver("ModifiedEvent", _onSliceNodeModified) + sliceNode2.AddObserver("ModifiedEvent", _onSliceNodeModified) + + composite1 = sliceWidget1.sliceLogic().GetSliceCompositeNode() + composite2 = sliceWidget2.sliceLogic().GetSliceCompositeNode() + + composite1.SetInteractionFlagsModifier(0) + composite2.SetInteractionFlagsModifier(0) + + composite1.AddObserver("ModifiedEvent", lambda caller, event: _onCompositeNodeModified(sliceNode1, caller, event)) diff --git a/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py b/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py index ba2128d..5ba75bd 100644 --- a/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py +++ b/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py @@ -3,8 +3,13 @@ import random import traceback import qt +import weakref +import pprint +import sys +from ltrace.slicer import helpers +from humanize import naturalsize from types import MappingProxyType - +from typing import List from ltrace.slicer.tests.constants import TestState from ltrace.slicer.tests.test_case import TestCase from ltrace.slicer.tests.utils import ( @@ -16,6 +21,7 @@ from ltrace.slicer.tests.widgets_identification import guess_widget_by_name, widgetsIdentificationModule from ltrace.utils.string_comparison import StringComparison from slicer import ScriptedLoadableModule +from stopit import ThreadingTimeout class LTracePluginTestMeta(type(qt.QObject), type(ScriptedLoadableModule.ScriptedLoadableModuleTest)): @@ -27,6 +33,7 @@ class LTracePluginTest(qt.QObject, ScriptedLoadableModule.ScriptedLoadableModule test_case_finished = qt.Signal(str, object) tests_cancelled = qt.Signal() + GLOBAL_TIMEOUT_MS = 5 * (60 * 1000) # 5 minutes def __init__( self, @@ -53,15 +60,16 @@ def __init__( self.__after_clear = after_clear test_case_method_list = self.get_test_case_methods() - self.__test_cases = self.__get_methods(test_case_method_list, test_case_filter) + self.__test_cases: List[TestCase] = self.__get_methods(test_case_method_list, test_case_filter) generate_method_list = self.get_generate_methods() - self.__generate_methods = self.__get_methods(generate_method_list, test_case_filter) + self.__generate_methods: List[TestCase] = self.__get_methods(generate_method_list, test_case_filter) self.__test_state = TestState.NOT_INITIALIZED self.__show_overview = show_overview self.__warnings = [] self.__widgets = {} + self.__timeout_timer = None @property def widgets(self): @@ -105,28 +113,36 @@ def reloadModuleWidget(self): """Wrapper for widget reload""" self._module_widget.onReload() - def setUp(self): - """ScriptedLoadableModuleTest method overload, called before starting the testing case.""" + def setUp(self, test_case: TestCase) -> None: + """Set up the test suite, called before starting the testing case. + + Args: + test_case (TestCase): the current TestCase object. + """ self.__close_project() try: - self._pre_setup() + self._pre_setup(test_case) except Exception as error: self.warnings.append(error) + old_widget = getattr(slicer.modules, self._module_name + "Widget", None) self._module_widget = slicer.util.getNewModuleWidget(self._module_name) + setattr(slicer.modules, self._module_name + "Widget", old_widget) + self._module_widget.parent.setWindowModality(qt.Qt.ApplicationModal) self._module_widget.parent.show() self._module_widget.enter() process_events() try: - self._post_setup() + self._post_setup(test_case) except Exception as error: self.warnings.append(error) def tearDown(self): try: + self.__uninstall_timeout_timer() self.tear_down() except Exception as error: message = "Test suite tear down failed! Please review the 'tear_down' method from the test class!" @@ -134,14 +150,42 @@ def tearDown(self): self.warnings.append(message) if self._module_widget is not None: + # Disconnect signals that keep the widget's method alive self._module_widget.cleanup() - self._module_widget.parent.close() - del self._module_widget + + # Delete the module widget immediately after processing events. + # deleteLater() only deletes after the tests are finished. + slicer.app.processEvents(1000) + self._module_widget.parent.delete() + slicer.app.processEvents(1000) + + weak_widget = weakref.ref(self._module_widget) self._module_widget = None + self.__widgets = {} + + try: + del sys.last_value + del sys.last_traceback + except AttributeError: + pass + + gc.collect() + if weak_widget() is not None: + refs = gc.get_referrers(weak_widget()) + refs_str = pprint.pformat(refs) + message = f"The module widget {weak_widget()} was not deleted properly!\n\nReferrers:\n{refs_str}" + self.warnings.append(message) + """ + Run pip_install("objgraph") and run the following code to generate a graph of the references: + import objgraph + + objgraph.show_backrefs( + [weak_widget()], max_depth=10, too_many=10, filename="objgraph.dot", shortnames=False + ) + """ if self.__after_clear: self.__close_project() - gc.collect() def cancel(self): if not self.__test_state == TestState.RUNNING: @@ -158,6 +202,9 @@ def runTest(self): show_window=False, ) + # Revert layout to conventional + slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + test_cases = self.test_cases + self.generate_methods if self.__shuffle: random.shuffle(test_cases) @@ -167,24 +214,29 @@ def runTest(self): if self.__test_state == TestState.CANCELLED and idx != len(test_cases) - 1: break - self.setUp() + self.setUp(test) log( " " * 4 + f"Running test case: {test.name}... ", end="", show_window=False, ) - test() + timeout_sec = (test.timeout_ms or self.GLOBAL_TIMEOUT_MS) // 1000 + with ThreadingTimeout(seconds=timeout_sec) as timeout_ctx: + test() + self.tearDown() if test.status == TestState.SUCCEED: valid_test_count += 1 - log(f"OK! [{test.elapsed_time_sec:.6f} sec]") + log(f"OK! [{test.elapsed_time_sec:.6f} sec]", end="") self.test_case_finished.emit(test.function.__name__, TestState.SUCCEED) else: - log(f"FAILED! [{test.elapsed_time_sec:.6f} sec]") + log(f"FAILED! [{test.elapsed_time_sec:.6f} sec]", end="") self.test_case_finished.emit(test.function.__name__, TestState.FAILED) if self.__break_on_failure: break + mem_usage = helpers.get_memory_usage() + log(f" - Memory usage: {naturalsize(mem_usage)}") if self.__test_state == TestState.CANCELLED and idx != len(test_cases) - 1: self.__on_test_cancelled() @@ -222,9 +274,12 @@ def post_setup(self): """ pass - def _pre_setup(self): + def _pre_setup(self, test_case: TestCase) -> None: """ Method responsible to handle setup before module initialization + + Args: + test_case (TestCase): the current TestCase object. """ try: self.pre_setup() @@ -233,9 +288,15 @@ def _pre_setup(self): message += f"\nError: {error}\n{traceback.format_exc()}" raise Exception(message) - def _post_setup(self): - """ - Method responsible to handle setup after module initialization + def _post_setup(self, test_case: TestCase) -> None: + """Method responsible to handle setup after module initialization + + Args: + test_case (TestCase): the current TestCase object. + + Raises: + Exception: When failing to automatically recognize widgets names from the module. + Exception: When the overloaded 'post_setup' (from the LTracePluginTest derived class) method fails. """ try: self.__widgets = widgetsIdentificationModule(self._module_widget).widgets @@ -250,6 +311,8 @@ def _post_setup(self): try: self.post_setup() + timeout_ms = test_case.timeout_ms or self.GLOBAL_TIMEOUT_MS + self.__install_timeout_timer(interval_ms=timeout_ms + 1500) # To trigger after ThreadingTimeout except Exception as error: message = "Test suite post-setup failed! Please review the 'post_setup' class method!" message += f"\nError: {error}\n{traceback.format_exc()}" @@ -270,8 +333,9 @@ def find_widget( self, name: str, obj=None, - type="QWidget", + _type="QWidget", comparison_type=StringComparison.EXACTLY, + only_visible=False, ): """Search for the QWidget that contains the desired object's name attribute. @@ -290,11 +354,11 @@ def find_widget( obj = self._module_widget.parent - return find_widget_by_object_name(obj, name, type, comparison_type) or guess_widget_by_name( + return find_widget_by_object_name(obj, name, _type, comparison_type, only_visible) or guess_widget_by_name( self.__widgets, name ) - def __get_methods(self, methods_list: list, methods_filter: list): + def __get_methods(self, methods_list: list, methods_filter: list) -> List[TestCase]: """Retrieve test or generate methods from the LTracePluginTest object. Args: @@ -350,3 +414,51 @@ def get_warnings_text(self) -> str: text += "\n" + "=" * 66 + "\n" return text + + def __install_timeout_timer(self, interval_ms: int = 1000) -> None: + """Install a timer that limits the test case duration. + + Args: + interval_ms (int, optional): The maximum duration in milliseconds. Defaults to 1000 ms. + """ + self.__timeout_timer = qt.QTimer() + self.__timeout_timer.setSingleShot(True) + self.__timeout_timer.timeout.connect(self.__on_timeout) + self.__timeout_timer.setInterval(interval_ms) + self.__timeout_timer.start() + + def __uninstall_timeout_timer(self) -> None: + """Uninstall the timer that limits the test case duration.""" + if self.__timeout_timer is None: + return + + self.__timeout_timer.stop() + self.__timeout_timer = None + + def __on_timeout(self) -> None: + """Method that handles the event that is triggered when the test case timeout is reached. + + Raises: + TimeoutError: When the test case timeout is reached. + """ + if self.__timeout_timer is None or self.__test_state != TestState.RUNNING: + return + + # Clear possible message boxes freezing the operation + message_boxes = slicer.util.mainWindow().findChildren(qt.QMessageBox) + message_from_boxes = [] + for message_box in message_boxes: + if not message_box.visible: + message_box.close() + continue + + message_from_boxes.append(message_box.text) + message_box.close() + + if message_from_boxes: + messages_formated = "\n".join(message_from_boxes) + message = "Test case timeout reached! Please check the test case logic.\n" + message += f"Messages from message boxes: {messages_formated}" + raise TimeoutError(message) + + raise TimeoutError("Test case timeout reached! Please check the test case logic.") diff --git a/src/ltrace/ltrace/slicer/tests/test_case.py b/src/ltrace/ltrace/slicer/tests/test_case.py index de14ecd..991bfb4 100644 --- a/src/ltrace/ltrace/slicer/tests/test_case.py +++ b/src/ltrace/ltrace/slicer/tests/test_case.py @@ -1,10 +1,11 @@ import sys import traceback +from inspect import signature from ltrace.slicer.helpers import ElapsedTime from ltrace.slicer.tests.constants import TestState +from typing import Callable, Union from unittest.mock import patch -from typing import Callable # Hack to catch exceptions triggered by Qt signals sys._excepthook = sys.excepthook @@ -14,14 +15,27 @@ class TestCase: """Class to handle information about a single test case.""" def __init__(self, function: Callable, cls: "LTracePluginTest") -> None: + assert function is not None, "Test case function is None!" self.name = self.__generate_name(function) self.function = function self.status: TestState = TestState.NOT_INITIALIZED self.reason = "" self.elapsed_time_sec = 0 self.test_module_class = cls + self.timeout_ms = self.__get_timeout() self.__exception_hook_patch = patch("sys.excepthook", self.__exception_hook_handler) + def __get_timeout(self) -> Union[int, None]: + """Get timeout in milliseconds. It will get the 'timeout_ms' parameter from the test case method if it is available. + Otherwise, it will use the default value from 'TestCase.DEFAULT_TIMEOUT_MS'. + + Returns: + int: the timeout in milliseconds. + """ + func_signature = signature(self.function) + timeout_ms_param = func_signature.parameters.get("timeout_ms") + return timeout_ms_param.default if timeout_ms_param is not None else None + def __generate_name(self, function: Callable) -> None: return function.__name__.replace("test_", "").replace("_", " ") diff --git a/src/ltrace/ltrace/slicer/tests/utils.py b/src/ltrace/ltrace/slicer/tests/utils.py index b48fa6c..a1182d9 100644 --- a/src/ltrace/ltrace/slicer/tests/utils.py +++ b/src/ltrace/ltrace/slicer/tests/utils.py @@ -9,6 +9,7 @@ from ltrace.slicer.project_manager import ProjectManager from ltrace.slicer.helpers import make_directory_writable, WatchSignal from pathlib import Path +from stopit import TimeoutException from typing import Union TEST_LOG_FILE_PATH = Path(slicer.app.temporaryPath) / "tests.log" @@ -49,26 +50,39 @@ def wait(seconds: float) -> None: """ start = time.perf_counter() - while True: - time.sleep(0.1) - process_events() + try: + while True: + time.sleep(0.1) + process_events() - if time.perf_counter() - start >= seconds: - break + if time.perf_counter() - start >= seconds: + break + except TimeoutException: + raise TimeoutError("Test timeout reached!") -def wait_cli_to_finish(cli_node): +def wait_cli_to_finish(cli_node, timeout_sec: int = 3600) -> None: """Lock thread until respective CLI node is not busy anymore. Args: - cli_node (vtkMRMLCommandLineModuleNode ): the CLI node object, + cli_node (vtkMRMLCommandLineModuleNode): the CLI node object, + timeout_sec (int, optional): the timeout in seconds. Defaults to 3600 seconds. """ if cli_node is None: return - while cli_node.IsBusy(): - time.sleep(0.200) - process_events() + start = time.perf_counter() + try: + while cli_node.IsBusy(): + time.sleep(0.200) + process_events() + + if time.perf_counter() - start >= timeout_sec: + cli_node.Cancel() + raise TimeoutError("CLI timeout reached!") + except TimeoutException: + cli_node.Cancel() + raise TimeoutError("Test timeout reached!") def log(message, show_window=False, end="\n"): @@ -84,7 +98,9 @@ def log(message, show_window=False, end="\n"): slicer.util.delayDisplay(message, autoCloseMsec=2000) -def find_widget_by_object_name(obj, name: str, _type="QWidget", comparison_type=StringComparison.EXACTLY): +def find_widget_by_object_name( + obj, name: str, _type="QWidget", comparison_type=StringComparison.EXACTLY, only_visible=False +): """Finds widgets inside qt objects. Please write a test when using this function""" if not obj: return None @@ -95,6 +111,9 @@ def find_widget_by_object_name(obj, name: str, _type="QWidget", comparison_type= return None for widget in widgets: + if only_visible and not widget.visible: + continue + if widget and widget.objectName == name: return widget diff --git a/src/ltrace/ltrace/slicer/tracking/tracker.py b/src/ltrace/ltrace/slicer/tracking/tracker.py deleted file mode 100644 index 32c7482..0000000 --- a/src/ltrace/ltrace/slicer/tracking/tracker.py +++ /dev/null @@ -1,23 +0,0 @@ -import logging - -from abc import ABC, abstractclassmethod - - -class Tracker(ABC): - def __init__(self) -> None: - super().__init__() - self.logger = logging.getLogger("tracking") - - def log(self, message: str) -> None: - if not message: - return - - self.logger.info(f"[{self.__class__.__name__}] {message}") - - @abstractclassmethod - def install(self) -> None: - pass - - @abstractclassmethod - def uninstall(self) -> None: - pass diff --git a/src/ltrace/ltrace/slicer/tracking/trackers/module_tracker.py b/src/ltrace/ltrace/slicer/tracking/trackers/module_tracker.py deleted file mode 100644 index 55b5479..0000000 --- a/src/ltrace/ltrace/slicer/tracking/trackers/module_tracker.py +++ /dev/null @@ -1,13 +0,0 @@ -from ltrace.slicer.application_observables import ApplicationObservables -from ltrace.slicer.tracking.tracker import Tracker - - -class ModuleTracker(Tracker): - def install(self) -> None: - ApplicationObservables().moduleWidgetEnter.connect(self.__onLog) - - def uninstall(self) -> None: - ApplicationObservables().moduleWidgetEnter.disconnect(self.__onLog) - - def __onLog(self, moduleObject): - self.log(f"Changed to module: {moduleObject.moduleName}") diff --git a/src/ltrace/ltrace/slicer/tracking/trackers/volume_node_tracker.py b/src/ltrace/ltrace/slicer/tracking/trackers/volume_node_tracker.py deleted file mode 100644 index ec60925..0000000 --- a/src/ltrace/ltrace/slicer/tracking/trackers/volume_node_tracker.py +++ /dev/null @@ -1,70 +0,0 @@ -from ltrace.slicer.node_observer import NodeObserver -from ltrace.slicer import helpers -from ltrace.slicer.tracking.tracker import Tracker - -import vtk -import slicer -from typing import Dict - - -class VolumeNodeTracker(Tracker): - def __init__(self) -> None: - super().__init__() - self.__node_observers = [] - - def __getNodeData(self, node: slicer.vtkMRMLNode) -> dict: - if node is None: - return {} - - return { - "name": node.GetName(), - "id": node.GetID(), - "type": node.GetClassName(), - "shape": node.GetImageData().GetDimensions() if node.GetImageData() is not None else None, - "spacing": node.GetImageData().GetSpacing() if node.GetImageData() is not None else None, - "origin": node.GetImageData().GetOrigin() if node.GetImageData() is not None else None, - "data_type": helpers.getScalarTypesAsString(node.GetImageData().GetScalarType()) - if node.GetImageData() is not None - else None, - } - - def __onNodeModified(self, node_observer: NodeObserver, node: slicer.vtkMRMLNode): - self.log(f"Volume node modified: {self.__getNodeData(node)}") - - def __onNodeRemoved(self, node_observer: NodeObserver, node: slicer.vtkMRMLNode): - if node_observer not in self.__node_observers: - return - - self.__node_observers.remove(node_observer) - self.log(f"Volume node removed: {self.__getNodeData(node)}") - - @vtk.calldata_type(vtk.VTK_OBJECT) - def __on_node_added(self, caller, eventId, node): - if ( - isinstance(node, slicer.vtkMRMLLabelMapVolumeNode) - or isinstance(node, slicer.vtkMRMLScalarVolumeNode) - or isinstance(node, slicer.vtkMRMLVectorVolumeNode) - ): - observer = NodeObserver(node=node, parent=slicer.util.mainWindow()) - observer.modifiedSignal.connect(self.__onNodeModified) - observer.removedSignal.connect(self.__onNodeRemoved) - - self.__node_observers.append(observer) - - self.log(f"Volume node added: {self.__getNodeData(node)}") - - def install(self) -> None: - self.nodeAddedObserverHandler = slicer.mrmlScene.AddObserver( - slicer.mrmlScene.NodeAddedEvent, self.__on_node_added - ) - - def uninstall(self) -> None: - if not self.nodeAddedObserverHandler: - return - - self.nodeAddedObserverHandler = slicer.mrmlScene.RemoveObserver(self.nodeAddedObserverHandler) - for node_observer in self.__node_observers[:]: - node_observer.clear() - self.__node_observers.remove(node_observer) - - self.__node_observers.clear() diff --git a/src/ltrace/ltrace/slicer/tracking/trackers/widget_tracker.py b/src/ltrace/ltrace/slicer/tracking/trackers/widget_tracker.py deleted file mode 100644 index 072657f..0000000 --- a/src/ltrace/ltrace/slicer/tracking/trackers/widget_tracker.py +++ /dev/null @@ -1,561 +0,0 @@ -import ctk -import logging -import qt -import slicer -import re - -from abc import abstractclassmethod -from dataclasses import dataclass -from ltrace.slicer.application_observables import ApplicationObservables -from ltrace.slicer.tracking.tracker import Tracker -from ltrace.slicer_utils import LTracePluginWidget -from typing import Dict - - -@dataclass -class SignalTracking: - widget: qt.QWidget - signal: qt.Signal - callback: object - - def __get_signal_object(self): - signal_str = self.signal.__name__.split("(")[0] - if not self.widget or not self.callback or not hasattr(self.widget, signal_str): - return None - - return getattr(self.widget, signal_str, None) - - def __post_init__(self) -> None: - self.connect() - - def connect(self): - signal_obj = self.__get_signal_object() - if not signal_obj: - raise AttributeError( - f"Invalid signal parameters. Widget: {self.widget}, Signal: {self.signal}, Callback {self.callback}" - ) - - signal_obj.connect(self.callback) - - def __del__(self): - self.disconnect() - - def disconnect(self): - signal_obj = self.__get_signal_object() - if not signal_obj: - return - - signal_obj.disconnect(self.callback) - - -def getAllWidgets(widget): - widgets = [widget] - try: - if isinstance(widget, tuple): - for child in widget: - if not hasattr(child, "children"): - continue - - widgets += getAllWidgets(child) - else: - for child in widget.children(): - if not hasattr(child, "children"): - continue - - widgets += getAllWidgets(child) - except Exception as error: - logging.warning(error) - - return list(widgets) - - -def getPluginFromWidget(widget): - currentWidget = widget - while currentWidget.parent() is not None: - if isinstance(currentWidget.parent(), LTracePluginWidget) or isinstance( - currentWidget.parent(), slicer.qSlicerScriptedLoadableModuleWidget - ): - return currentWidget.parent() - - currentWidget = currentWidget.parent() - - return None - - -def getBuddyLabel(widget) -> str: - widgets = [] - - def formatLabelText(text): - return re.sub(r":", "", text).strip() - - module_widget = getPluginFromWidget(widget) - if module_widget: - widgets.extend(getAllWidgets(module_widget.children())) - - labels = [wid for wid in widgets if isinstance(wid, qt.QLabel) and wid.isVisible()] - labelText = "" - rect = widget.geometry - point = widget.parentWidget().mapToGlobal(widget.pos) - globalRect = qt.QRect(point, rect.size()) - xDistance = globalRect.left() - globalRect.adjust(-xDistance, 0, 0, 0) - - for label in labels: - if label.buddy() == widget: - labelText = formatLabelText(label.text) - break - - labelRect = label.geometry - labelRectPoint = label.parentWidget().mapToGlobal(label.pos) - globalLabelRect = qt.QRect(labelRectPoint, labelRect.size()) - - if globalRect.intersects(globalLabelRect) and hasattr(label, "text"): - if len(label.text) < len(labelText): - continue - - labelText = formatLabelText(label.text) - - return labelText - - -def getValidObjectName(widget): - return widget.objectName if widget.objectName and not widget.objectName.startswith("qt_") else "" - - -class WidgetTrackerHandler(qt.QObject): - logSignal = qt.Signal(str) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.signalTracking: SignalTracking = None - - @abstractclassmethod - def handleEventFilter(self, obj: object, event: qt.QEvent) -> bool: - """ - Handle the event filter for the given object. - Args: - obj: The object to handle the event filter for. - Returns: - None - """ - pass - - @abstractclassmethod - def toDict(self, *args, **kwargs) -> Dict: - """Convert the given object to a dictionary. - - Returns: - Dict: the object description as dict - """ - pass - - -class ButtonWidgetTracker(WidgetTrackerHandler): - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - - if event.type() != qt.QEvent.MouseButtonRelease or not ( - isinstance(obj, qt.QPushButton) - or isinstance(obj, qt.QToolButton) - or ("Button" in obj.__class__.__name__ and not isinstance(obj, (ctk.ctkCollapsibleButton, qt.QRadioButton))) - ): - return - - infoDict = self.toDict(obj=obj) - if not infoDict: - return - - self.logSignal.emit(f"Button clicked: {infoDict}") - - def toDict(self, obj: object) -> Dict: - return { - "name": obj.objectName or obj.text or qt.QTextDocumentFragment.fromHtml(obj.toolTip).toPlainText(), - "isChecked": obj.isChecked(), - } - - -class MenuWidgetTracker(WidgetTrackerHandler): - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - - if event.type() != qt.QEvent.MouseButtonRelease or not isinstance(obj, qt.QMenu): - return - - infoDict = self.toDict(obj=obj, event=event) - if not infoDict: - return - - self.logSignal.emit(f"Menu action clicked: {infoDict}") - - def toDict(self, obj: object, event: qt.QEvent) -> Dict: - name = obj.objectName or obj.text if hasattr(obj, "text") else obj.title if hasattr(obj, "title") else "" - action = obj.actionAt(qt.QPoint(int(event.localPos().x()), int(event.localPos().y()))) - actionName = action.text if action is not None else "None" - return {"name": name, "action": actionName} - - -class CollapsibleButtonWidgetTracker(WidgetTrackerHandler): - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - - if event.type() != qt.QEvent.MouseButtonRelease or not isinstance(obj, ctk.ctkCollapsibleButton): - return - - infoDict = self.toDict(obj=obj) - if not infoDict: - return - - self.logSignal.emit(f"Collapsible button clicked: {infoDict}") - - def toDict(self, obj: object) -> Dict: - return {"name": getValidObjectName(obj) or obj.text, "collapsed": obj.collapsed} - - -class ComboBoxWidgetTrackerHandler(WidgetTrackerHandler): - def onCurrentTextChanged(self, text: str) -> None: - if not self.signalTracking: - return - - obj = self.signalTracking.widget - - infoDict = self.toDict(obj=obj) - if infoDict is not None: - self.logSignal.emit(f"Combo box changed: {infoDict}") - - if not obj.hasFocus(): - self.signalTracking.disconnect() - del self.signalTracking - self.signalTracking = None - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() == qt.QEvent.MouseButtonPress: - if isinstance(obj, qt.QComboBox) or issubclass(obj.__class__, qt.QComboBox): - self.signalTracking = SignalTracking( - widget=obj, signal=obj.currentTextChanged, callback=self.onCurrentTextChanged - ) - - if event.type() == qt.QEvent.MouseButtonRelease: - comboBox = None - if obj.__class__.__name__ == "QComboBoxPrivateContainer": - comboBox = obj.parentWidget() - elif hasattr(obj, "parentWidget") and obj.parentWidget().__class__.__name__ == "QComboBoxPrivateContainer": - comboBox = obj.parentWidget().parentWidget() - - if comboBox: - self.signalTracking = SignalTracking( - widget=comboBox, signal=comboBox.currentTextChanged, callback=self.onCurrentTextChanged - ) - - def toDict(self, obj: object) -> Dict: - return {"name": getValidObjectName(obj) or getBuddyLabel(obj), "text": obj.currentText} - - -class HierarchyComboBoxWidgetTrackerHandler(WidgetTrackerHandler): - def onHierarchyItemChanged(self) -> None: - if not self.signalTracking: - return - - obj = self.signalTracking.widget - - infoDict = self.toDict(obj=obj) - if not infoDict: - return - - self.logSignal.emit(f"Hierarchy combo box changed: {infoDict}") - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease: - return - - try: - comboBox = obj.parentWidget().parentWidget().parentWidget() - except Exception: - return - - if not ( - isinstance(comboBox, slicer.qMRMLSubjectHierarchyComboBox) - or issubclass(comboBox.__class__, slicer.qMRMLSubjectHierarchyComboBox) - ): - return - - self.signalTracking = SignalTracking( - widget=comboBox, signal=comboBox.currentItemChanged, callback=self.onHierarchyItemChanged - ) - - def toDict(self, obj: object) -> Dict: - subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - itemId = obj.currentItem() - node = subjectHierarchyNode.GetItemDataNode(itemId) - - return { - "name": getValidObjectName(obj) or getBuddyLabel(obj), - "selectedNodeName": node.GetName() if node is not None else "None", - "selectedNodeId": node.GetID() if node is not None else "None", - } - - -class SpinBoxWidgetTrackerHandler(WidgetTrackerHandler): - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not ( - isinstance(obj, qt.QSpinBox) or isinstance(obj, qt.QDoubleSpinBox) - ): - return - - infoDict = self.toDict(obj=obj) - if not infoDict: - return - - self.logSignal.emit(f"Spin box clicked: {infoDict}") - - def toDict(self, obj: object) -> Dict: - return {"name": getValidObjectName(obj) or getBuddyLabel(obj), "value": obj.value} - - -class TabBarWidgetTrackingHandler(WidgetTrackerHandler): - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not isinstance(obj, qt.QTabBar): - return - - infoDict = self.toDict(obj=obj) - if not infoDict: - return - - self.logSignal.emit(f"Tab clicked: {infoDict}") - - def toDict(self, obj: object) -> Dict: - return {"text": obj.tabText(obj.currentIndex)} - - -class LineEditWidgetTrackerHandler(WidgetTrackerHandler): - def onLineEditingFinished(self): - if not self.signalTracking: - return - - obj = self.signalTracking.widget - - if not obj.hasFocus(): - infoDict = self.toDict(obj=obj) - if infoDict is not None: - self.logSignal.emit(f"Line edit changed: {infoDict}") - - self.signalTracking.disconnect() - del self.signalTracking - self.signalTracking = None - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not isinstance(obj, qt.QLineEdit): - return - - self.signalTracking = SignalTracking( - widget=obj, signal=obj.editingFinished, callback=self.onLineEditingFinished - ) - - def toDict(self, obj: object) -> Dict: - return { - "name": getValidObjectName(obj) or getBuddyLabel(obj), - "text": obj.text, - } - - -class ListWidgetTrackingHandler(WidgetTrackerHandler): - def onListWidgetItemClicked(self, item: qt.QListWidgetItem) -> None: - if not self.signalTracking: - return - - obj = self.signalTracking.widget - - infoDict = self.toDict(obj=obj, item=item) - if infoDict is not None: - self.logSignal.emit(f"List Item clicked: {infoDict}") - - self.signalTracking.disconnect() - del self.signalTracking - self.signalTracking = None - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not isinstance(obj.parentWidget(), qt.QListWidget): - return - - listWidget = obj.parentWidget() - self.signalTracking = SignalTracking( - widget=listWidget, signal=listWidget.itemClicked, callback=self.onListWidgetItemClicked - ) - - def toDict(self, obj: object, item: qt.QListWidgetItem) -> Dict: - return { - "name": getValidObjectName(obj) or getBuddyLabel(obj), - "itemText": item.text(), - "checked": True if item.checkState() == qt.Qt.Checked else False, - "selected": item.isSelected(), - } - - -class TableWidgetTrackingHandler(WidgetTrackerHandler): - def onTableWidgetItemClicked(self, item: qt.QTableWidgetItem) -> None: - if not self.signalTracking: - return - - obj = self.signalTracking.widget - - infoDict = self.toDict(obj=obj, item=item) - if infoDict is not None: - self.logSignal.emit(f"Table Item clicked: {infoDict}") - self.signalTracking.disconnect() - del self.signalTracking - self.signalTracking = None - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not ( - isinstance(obj.parentWidget(), qt.QTableWidget) or issubclass(obj.parentWidget().__class__, qt.QTableWidget) - ): - return - - tableWidget = obj.parentWidget() - infoDict = self.toDict(obj=tableWidget) - if not infoDict: - return - - self.logSignal.emit(f"Table cell clicked: {infoDict}") - - def toDict(self, obj: object) -> Dict: - tableItem = obj.currentItem() - if tableItem is None: - return None - - row = tableItem.row() - column = tableItem.column() - - rowName = obj.verticalHeaderItem(row).text() if obj.verticalHeaderItem(row) is not None else row - columnName = obj.horizontalHeaderItem(column).text() if obj.horizontalHeaderItem(column) is not None else column - - return { - "name": getValidObjectName(obj) or getBuddyLabel(obj), - "itemText": tableItem.text(), - "row": rowName, - "column": columnName, - } - - -class CheckBoxWidgetTrackingHandler(WidgetTrackerHandler): - def onCheckBoxToggled(self, checked: bool = None) -> None: - if not self.signalTracking: - return - - obj = self.signalTracking.widget - infoDict = self.toDict(obj=obj) - if infoDict is not None: - self.logSignal.emit(f"Check box toggled: {infoDict}") - - if not obj.hasFocus(): - self.signalTracking.disconnect() - del self.signalTracking - self.signalTracking = None - - def handleEventFilter(self, obj: object, event: qt.QEvent) -> None: - if event.type() != qt.QEvent.MouseButtonRelease or not ( - isinstance(obj, qt.QCheckBox) or isinstance(obj, qt.QRadioButton) - ): - return - - self.signalTracking = SignalTracking(widget=obj, signal=obj.toggled, callback=self.onCheckBoxToggled) - - def toDict(self, obj: object) -> Dict: - return { - "name": obj.text or getBuddyLabel(obj), - "checked": obj.isChecked(), - } - - -class WidgetTrackerEventFilter(qt.QObject): - logSignal = qt.Signal(str) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.handlers = [] - self.installHandlers() - - def installHandlers(self): - handlers = [ - ButtonWidgetTracker(), - MenuWidgetTracker(), - CollapsibleButtonWidgetTracker(), - ComboBoxWidgetTrackerHandler(), - HierarchyComboBoxWidgetTrackerHandler(), - SpinBoxWidgetTrackerHandler(), - TabBarWidgetTrackingHandler(), - LineEditWidgetTrackerHandler(), - ListWidgetTrackingHandler(), - TableWidgetTrackingHandler(), - CheckBoxWidgetTrackingHandler(), - ] - - for handler in handlers: - handler.logSignal.connect(self.logSignal) - - self.handlers = handlers - - def eventFilter(self, obj, event) -> bool: - for handler in self.handlers: - handler.handleEventFilter(obj, event) - - return False - - -class WidgetTracker(Tracker): - def __init__(self) -> None: - super().__init__() - self.eventFilter = WidgetTrackerEventFilter() - self.currentModuleWidget = None - - def onChangeModule(self, moduleObject=None): - if self.currentModuleWidget: - self.currentModuleWidget.removeEventFilter(self.eventFilter) - - if issubclass(moduleObject.__class__, LTracePluginWidget): - self.currentModuleWidget = moduleObject.parent - moduleName = moduleObject.moduleName - else: - if not moduleObject: - moduleName = slicer.util.selectedModule().lower() - else: - moduleName = moduleObject.moduleName.lower() - - module = getattr(slicer.modules, moduleName, None) - - if not module: - return - self.currentModuleWidget = ( - module.widgetRepresentation() if hasattr(module, "widgetRepresentation") else None - ) - - if not self.currentModuleWidget: - return - - widgets = getAllWidgets(self.currentModuleWidget) - for wid in widgets: - wid.installEventFilter(self.eventFilter) - - def install(self) -> None: - self.onChangeModule() - ApplicationObservables().moduleWidgetEnter.connect(self.onChangeModule) - - mainWindow = slicer.util.mainWindow() - widgets = getAllWidgets(mainWindow) - for widget in widgets: - if not hasattr(widget, "installEventFilter"): - continue - - widget.installEventFilter(self.eventFilter) - - self.eventFilter.logSignal.connect(self.log) - - def uninstall(self): - if self.currentModuleWidget: - self.currentModuleWidget.removeEventFilter(self.eventFilter) - - mainWindow = slicer.util.mainWindow() - widgets = getAllWidgets(mainWindow) - for widget in widgets: - if not hasattr(widget, "removeEventFilter"): - continue - widget.removeEventFilter(self.eventFilter) - - self.eventFilter.logSignal.disconnect(self.log) - ApplicationObservables().moduleWidgetEnter.disconnect(self.onChangeModule) diff --git a/src/ltrace/ltrace/slicer/tracking/tracking_manager.py b/src/ltrace/ltrace/slicer/tracking/tracking_manager.py deleted file mode 100644 index 44b2b68..0000000 --- a/src/ltrace/ltrace/slicer/tracking/tracking_manager.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -import slicer - -from datetime import datetime -from ltrace.slicer.tracking.trackers.widget_tracker import WidgetTracker -from ltrace.slicer.tracking.trackers.module_tracker import ModuleTracker -from ltrace.slicer.tracking.trackers.volume_node_tracker import VolumeNodeTracker -from typing import List -from pathlib import Path - - -class TrackingManager: - def __init__(self) -> None: - self.__trackers = [WidgetTracker(), ModuleTracker(), VolumeNodeTracker()] - self._createLogger() - - def _createLogger(self) -> None: - """Create logger for the tracking feature.""" - logger = logging.getLogger("tracking") - - # Remove any previous handler - for handler in logger.handlers: - logger.removeHandler(handler) - - logger.propagate = False - - # Create and add the file handler - formatter = logging.Formatter( - "[%(levelname)s] %(asctime)s - %(name)s: %(message)s", - datefmt="%d/%m/%Y %I:%M:%S%p", - ) - - now = datetime.now() - datetimeString = now.strftime("%Y%m%d_%H%M%S_%f") - logFilePath = Path(slicer.app.temporaryPath) / f"tracking_{datetimeString}.log" - fileHandler = logging.FileHandler(logFilePath, mode="a") - fileHandler.setLevel(logging.INFO) - fileHandler.setFormatter(formatter) - logger.addHandler(fileHandler) - - def installTrackers(self) -> None: - """Install all trackers.""" - for tracker in self.__trackers: - tracker.install() - - def uninstallTrackers(self) -> None: - """Uninstall all trackers.""" - for tracker in self.__trackers: - tracker.uninstall() - - def getRecentLogs(self) -> List[Path]: - """Retrieve the most recent logs - - Returns: - List[Path]: The list of Path object related to the recent logs. - """ - userSettings = slicer.app.userSettings() - filesNumber = userSettings.value("LogFiles/NumberOfFilesToKeep") - filesList = list(Path(slicer.app.temporaryPath).glob("tracking_*.log")) - filesList.sort(key=lambda x: x.stat().st_mtime, reverse=True) - - return filesList[:filesNumber] diff --git a/src/ltrace/ltrace/slicer/undo.py b/src/ltrace/ltrace/slicer/undo.py index 7567f20..84030d6 100644 --- a/src/ltrace/ltrace/slicer/undo.py +++ b/src/ltrace/ltrace/slicer/undo.py @@ -72,7 +72,7 @@ def undo(self, volumeNode): if verify: saved_hash = saved_states[current_stage]["current_hash"] - current_hash = hashlib.sha256(origin_array).hexdigest() + current_hash = hashlib.sha1(origin_array).hexdigest() if current_hash != saved_hash: print("Cannot undo, array has unsaved changes") saved_states[:] = [] @@ -99,7 +99,7 @@ def redo(self, volumeNode): origin_array = util.arrayFromVolume(volumeNode) if verify: - current_hash = hashlib.sha256(origin_array).hexdigest() + current_hash = hashlib.sha1(origin_array).hexdigest() try: saved_hash = saved_states[current_stage]["current_hash"] if current_hash != saved_hash: @@ -133,7 +133,7 @@ def modify_and_save(self, volumeNode, new_array, bbox_slices=None): util.arrayFromVolumeModified(volumeNode) if save_hash: - current_hash = hashlib.sha256(origin_array).hexdigest() + current_hash = hashlib.sha1(origin_array).hexdigest() else: current_hash = None diff --git a/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py b/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py index fe375ac..6e366be 100644 --- a/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py +++ b/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py @@ -92,5 +92,11 @@ def clearSelection(self): self.setProperty("defaultText", self.customDefaultText) def __onEndCloseScene(self, *args): + try: + self.children() + except ValueError: + # Widget was destroyed + slicer.mrmlScene.RemoveObserver(self.end_close_scene_observer_handler) + return if self.customDefaultText: self.setProperty("defaultText", self.customDefaultText) diff --git a/src/ltrace/ltrace/slicer/widget/histogram_popup.py b/src/ltrace/ltrace/slicer/widget/histogram_popup.py index cccdb0b..2103e07 100644 --- a/src/ltrace/ltrace/slicer/widget/histogram_popup.py +++ b/src/ltrace/ltrace/slicer/widget/histogram_popup.py @@ -23,7 +23,7 @@ def setup(self): self.mainInput = ui.volumeInput(hasNone=True, onChange=self.setNode, onActivation=self.setNode) contentsFrameLayout.addRow("Node:", self.mainInput) - volumesWidget = slicer.modules.volumes.widgetRepresentation() + volumesWidget = slicer.modules.volumes.createNewWidgetRepresentation() self.activeVolumeNodeSelector = volumesWidget.findChild(slicer.qMRMLNodeComboBox, "ActiveVolumeNodeSelector") diff --git a/src/ltrace/ltrace/slicer/widgets.py b/src/ltrace/ltrace/slicer/widgets.py index fb6ea15..1f347c2 100644 --- a/src/ltrace/ltrace/slicer/widgets.py +++ b/src/ltrace/ltrace/slicer/widgets.py @@ -15,8 +15,6 @@ from typing import Union, List, Tuple -from ltrace.slicer_utils import print_debug - def findSOISegmentID(segmentation): n_segments = segmentation.GetNumberOfSegments() @@ -113,6 +111,9 @@ def _value_changed(self, input_value): class SingleShotInputWidget(qt.QWidget): segmentSelectionChanged = qt.Signal(list) segmentListUpdated = qt.Signal(tuple, dict) + onMainSelectedSignal = qt.Signal(slicer.vtkMRMLVolumeNode) + onReferenceSelectedSignal = qt.Signal(slicer.vtkMRMLVolumeNode) + onSoiSelectedSignal = qt.Signal(slicer.vtkMRMLVolumeNode) MODE_NAME = "Single-Shot" @@ -156,10 +157,6 @@ def __init__( self.hasOverlappingLayers = False - self.onMainSelected = lambda volume: None - self.onReferenceSelected = lambda volume: None - self.onSoiSelected = lambda volume: None - self.inputVoxelSize = 1 self.formLayout = qt.QFormLayout(self) @@ -222,12 +219,16 @@ def __init__( self.segmentationLabel = self.formLayout.labelForField(self.targetBox) self.soiLabel = qt.QLabel(f'{rowTitles["soi"]}: ') - if not hideSoi: - self.formLayout.addRow(self.soiLabel, self.soiInput) + self.formLayout.addRow(self.soiLabel, self.soiInput) + if hideSoi: + self.soiLabel.hide() + self.soiInput.hide() self.referenceLabel = qt.QLabel(f'{rowTitles["reference"]}: ') - if not hideImage: - self.formLayout.addRow(self.referenceLabel, self.referenceInput) + self.formLayout.addRow(self.referenceLabel, self.referenceInput) + if hideImage: + self.referenceLabel.hide() + self.referenceInput.hide() self.requireSourceVolume = requireSourceVolume self.segmentsContainerWidget = ctk.ctkCollapsibleButton() @@ -259,10 +260,10 @@ def __init__( autoPorosityCalcLayout.addWidget(self.autoPorosityCalcCb) autoPorosityCalcLayout.addWidget(self.progressInput) autoPorosityCalcLayout.addStretch(1) - self.autoPorosityCalcWidget.visible = not self.hideCalcProp ## end autoPorosityCalcWidget segmentsLayout.addRow(self.autoPorosityCalcWidget) + self.autoPorosityCalcWidget.visible = not self.hideCalcProp self.segmentListGroup[1].itemChanged.connect(self.checkSelection) @@ -368,7 +369,7 @@ def updateRefNode(self, node): self.referenceInput.blockSignals(True) self.referenceInput.setCurrentNode(node) self.referenceInput.blockSignals(False) - self.referenceInput.setStyleSheet("{}") + self.referenceInput.setStyleSheet("") self._onReferenceSelected(node) def _onMainSelected(self, item): @@ -380,7 +381,7 @@ def _onMainSelected(self, item): self.resetUI() if self.autoReferenceFetch: self.updateRefNode(None) - self.onMainSelected(None) + self.onMainSelectedSignal.emit(None) self.autoPorosityCalcWidget.hide() self.segmentsContainerWidget.collapsed = True return @@ -405,7 +406,7 @@ def _onMainSelected(self, item): if self.autoReferenceFetch: self.updateRefNode(referenceNode) - self.onMainSelected(node) + self.onMainSelectedSignal.emit(node) if isLabeledData(node): self.updateSegmentList( @@ -428,7 +429,7 @@ def _onSOISelected(self, item): # If using pre-trained classifier if mainNode is None: self.autoPorosityCalcWidget.hide() - self.onSoiSelected(node) + self.onSoiSelectedSignal.emit(node) return referenceNode = helpers.getSourceVolume(mainNode) @@ -448,7 +449,7 @@ def _onSOISelected(self, item): return_proportions=self.autoPorosityCalcCb.isChecked(), ) ) - self.onSoiSelected(None) + self.onSoiSelectedSignal.emit(None) return if node.GetID() == mainNode.GetID(): @@ -456,7 +457,7 @@ def _onSOISelected(self, item): "The Segment of Interest (SOI) Node must be " "different from the Segmentation input." ) self.soiInput.setCurrentNode(None) - self.onSoiSelected(None) + self.onSoiSelectedSignal.emit(None) return soi = node.GetSegmentation() @@ -470,7 +471,7 @@ def _onSOISelected(self, item): ) if not yesOrNo: self.soiInput.setCurrentNode(None) - self.onSoiSelected(None) + self.onSoiSelectedSignal.emit(None) return if isLabeledData(mainNode): @@ -487,12 +488,12 @@ def _onSOISelected(self, item): ) ) - self.onSoiSelected(node) + self.onSoiSelectedSignal.emit(node) except TypeError as ter: pass # TODO check who escapes here? except Exception as rex: - print_debug(f"Failed to update SOI: {repr(rex)}", channel=logging.warning) + logging.error(repr(rex)) finally: self.progressInput.setText("") @@ -502,7 +503,7 @@ def _onReferenceSelected(self, _): node = self.referenceInput.currentNode() if node is None and mainNode is None: - self.onReferenceSelected(None) + self.onReferenceSelectedSignal.emit(None) return if node is None and mainNode and self.requireSourceVolume: @@ -510,7 +511,7 @@ def _onReferenceSelected(self, _): [s.hide() for s in self.segmentListGroup] self.dimensionsGroup.hide() self.toggleDimensions(None) - self.onReferenceSelected(None) + self.onReferenceSelectedSignal.emit(None) # NOTE: The timer is required to move the UI change to the main thread self.referenceInput.blockSignals(True) helpers.highlight_error(self.referenceInput, "QComboBox") @@ -541,7 +542,7 @@ def _onReferenceSelected(self, _): ) ) self.toggleDimensions(node) - self.onReferenceSelected(node) + self.onReferenceSelectedSignal.emit(node) except Exception as rex: logging.error(repr(rex)) finally: @@ -645,38 +646,44 @@ def ColoredIcon(r, g, b): class BatchInputWidget(qt.QWidget): MODE_NAME = "Batch" - def __init__(self, parent=None, settingKey="BatchInputWidget"): + def __init__(self, parent=None, settingKey="BatchInputWidget", objectNamePrefix=None): super().__init__(parent) formLayout = qt.QFormLayout(self) self.onDirSelected = lambda volume: None + self.ioFileInputLabel = qt.QLabel("Directory: ") self.ioFileInputLineEdit = ctk.ctkPathLineEdit() self.ioFileInputLineEdit.filters = ctk.ctkPathLineEdit.Dirs self.ioFileInputLineEdit.settingKey = settingKey + self.ioFileInputLineEdit.objectName = "Path Line Edit" + self.ioBatchROITagLabel = qt.QLabel("ROI Tag (.nrrd): ") self.ioBatchROITagPattern = qt.QLineEdit() self.ioBatchROITagPattern.text = "ROI" + self.ioBatchSegTagLabel = qt.QLabel("Segmentation Tag (.nrrd): ") self.ioBatchSegTagPattern = qt.QLineEdit() self.ioBatchSegTagPattern.text = "SEG" + self.ioBatchValTagLabel = qt.QLabel("Image Tag (.tif): ") self.ioBatchValTagPattern = qt.QLineEdit() self.ioBatchValTagPattern.text = "" + self.ioBatchLabelLabel = qt.QLabel("Target Segment Tag: ") self.ioBatchLabelPattern = qt.QLineEdit() self.ioBatchLabelPattern.text = "Poro" - formLayout.addRow("Directory: ", self.ioFileInputLineEdit) + formLayout.addRow(self.ioFileInputLabel, self.ioFileInputLineEdit) self.ioFileInputLineEdit.setToolTip("Select the directory where the input data/projects are saved.") - formLayout.addRow("Segmentation Tag (.nrrd): ", self.ioBatchSegTagPattern) + formLayout.addRow(self.ioBatchSegTagLabel, self.ioBatchSegTagPattern) self.ioBatchSegTagPattern.setToolTip("Type the Tag that identifies the Segmentation.") - formLayout.addRow("ROI Tag (.nrrd): ", self.ioBatchROITagPattern) + formLayout.addRow(self.ioBatchROITagLabel, self.ioBatchROITagPattern) self.ioBatchROITagPattern.setToolTip("Type the Tag that identifies the Region (SOI) Segment of Interest.") - formLayout.addRow("Image Tag (.tif): ", self.ioBatchValTagPattern) + formLayout.addRow(self.ioBatchValTagLabel, self.ioBatchValTagPattern) self.ioBatchValTagPattern.setToolTip("Type the Tag that identifies input Image.") - formLayout.addRow("Target Segment Tag: ", self.ioBatchLabelPattern) + formLayout.addRow(self.ioBatchLabelLabel, self.ioBatchLabelPattern) self.ioBatchLabelPattern.setToolTip( "Type the Tag that identifies the segment of the Segmentation to be inspected/partitioned." ) @@ -685,6 +692,9 @@ def __init__(self, parent=None, settingKey="BatchInputWidget"): self.ioFileInputLineEdit.connect("currentPathChanged(QString)", self._onDirSelected) self.inputVoxelSize = 1 # maintain interface + if objectNamePrefix is not None: + self.ioFileInputLineEdit.objectName = f"{objectNamePrefix} {self.ioFileInputLineEdit.objectName}" + def _onDirSelected(self, dirpath): self.onDirSelected(dirpath) diff --git a/src/ltrace/ltrace/slicer_utils.py b/src/ltrace/ltrace/slicer_utils.py index dba3362..7035c13 100644 --- a/src/ltrace/ltrace/slicer_utils.py +++ b/src/ltrace/ltrace/slicer_utils.py @@ -1,20 +1,16 @@ -import os import json -import logging from pathlib import Path +from typing import Union import markdown2 as markdown import numpy as np -import qt -import slicer -import vtk -from slicer import ScriptedLoadableModule +import pandas as pd from SegmentEditorEffects import * +from slicer import ScriptedLoadableModule from ltrace.slicer.application_observables import ApplicationObservables from ltrace.slicer.tests.ltrace_plugin_test import LTracePluginTest from ltrace.slicer.tests.ltrace_tests_widget import LTraceTestsWidget -from typing import Union, Callable __all__ = [ "LTracePlugin", @@ -33,8 +29,8 @@ def SetSourceVolumeIntensityMaskOff(self): try: parameterSetNode = self.scriptedEffect.parameterSetNode() parameterSetNode.SourceVolumeIntensityMaskOff() - except Exception as e: - pass + except Exception as error: + logging.debug(f"Error {error}. Traceback:\n{traceback.format_exc()}") class LTracePlugin(ScriptedLoadableModule.ScriptedLoadableModule): @@ -99,6 +95,16 @@ def onReload(self) -> None: def enter(self) -> None: ApplicationObservables().moduleWidgetEnter.emit(self) + def cleanup(self): + super().cleanup() + slicer.app.moduleManager().moduleAboutToBeUnloaded.disconnect(self._onModuleAboutToBeUnloaded) + if slicer_is_in_developer_mode(): + self.testAction.triggered.disconnect() + self.reloadTestAction.triggered.disconnect() + + for pluginWidget in self.parent.findChildren("qSlicerScriptedLoadableModuleWidget"): + pluginWidget.setParent(None) + class LTracePluginLogicMeta(type(qt.QObject), type(ScriptedLoadableModule.ScriptedLoadableModuleLogic)): pass @@ -110,7 +116,7 @@ class LTracePluginLogic( metaclass=LTracePluginLogicMeta, ): def __init__(self, parent=None): - super(qt.QObject, self).__init__() + super(qt.QObject, self).__init__(parent) super(ScriptedLoadableModule.ScriptedLoadableModuleLogic, self).__init__() @@ -123,6 +129,12 @@ def is_tensorflow_gpu_enabled(): def dataFrameToTableNode(dataFrame, tableNode=None): + """ " + ================================================================================================= + DEPRECATED use dataFrameToTableNode from data_utils instead + ================================================================================================= + """ + def is_float(value): return value.dtype in [np.dtype("float32")] @@ -261,21 +273,11 @@ def addNodeToSubjectHierarchy(node, dirPaths: list = None): subjectHierarchyNode.SetItemParent(subjectHierarchyNode.GetItemByDataNode(node), parentDirID) -def print_debug(text, channel: Callable = logging.debug): - if not slicer_is_in_developer_mode(): - return - - channel(text) - - -def print_stack(): +def print_debug(text): if not slicer_is_in_developer_mode(): return - import traceback - - text = traceback.format_exc() - logging.debug(text) + print(text) def base_version(): @@ -302,6 +304,12 @@ def hide_nodes_of_type(mrml_node_type): def dataframeFromTable(tableNode): + """ + ================================================================================================= + DEPRECATED use tableNodeToDataFrame from data_utils instead + ================================================================================================= + """ + """Optimized version from slicer.util.dataframeFromTable Convert table node content to pandas dataframe. @@ -370,7 +378,6 @@ def imageToHtml(image: Union[qt.QImage, qt.QPixmap], imageFormat=None, quality=- except TypeError as e: raise e - buffer.close() html = f'' return html @@ -404,3 +411,39 @@ def loadImage( obj = imageReader.read() if cls == qt.QImage else qt.QPixmap.fromImageReader(imageReader) return obj + + +def tableWidgetToDataFrame(tableWidget: qt.QTableWidget) -> pd.DataFrame: + """Convert a QTableWidget to a pandas dataframe. + + Args: + tableWidget (qt.QTableWidget): The QTableWidget to convert. + + Returns: + pd.DataFrame: The pandas dataframe. + """ + columnHeaders = [ + tableWidget.horizontalHeaderItem(column).text() + for column in range(tableWidget.columnCount) + if tableWidget.horizontalHeaderItem(column) is not None + ] + if not columnHeaders: + columnHeaders = None + indexHeaders = [ + tableWidget.verticalHeaderItem(row).text() + for row in range(tableWidget.rowCount) + if tableWidget.verticalHeaderItem(row) is not None + ] + if not indexHeaders: + indexHeaders = None + + data = [] + for row in range(tableWidget.rowCount): + columnData = [] + for column in range(tableWidget.columnCount): + columnData.append(tableWidget.item(row, column).text()) + + data.append(columnData) + + df = pd.DataFrame(data, columns=columnHeaders, index=indexHeaders) + return df diff --git a/src/ltrace/ltrace/utils/CorrelatedLabelMapVolume.py b/src/ltrace/ltrace/utils/CorrelatedLabelMapVolume.py index ea757e9..6e0e612 100644 --- a/src/ltrace/ltrace/utils/CorrelatedLabelMapVolume.py +++ b/src/ltrace/ltrace/utils/CorrelatedLabelMapVolume.py @@ -131,14 +131,27 @@ def __process(self): if referenceNode is None: return + labelMapVolumeNode = None if isinstance(referenceNode, slicer.vtkMRMLSegmentationNode): if self._labelMapVolumeNodeId is None: labelMapVolumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") self._labelMapVolumeNodeId = labelMapVolumeNode.GetID() - slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode( - referenceNode, self.labelMapVolumeNode, slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY - ) + seg = referenceNode.GetSegmentation() + empty = seg.GetNumberOfSegments() == 0 + + if not empty: + try: + seg_id = seg.GetNthSegmentID(0) + array = slicer.util.arrayFromSegmentInternalBinaryLabelmap(referenceNode, seg_id) + empty = array.max() <= 0 + except AttributeError: + empty = True + + if not empty: + slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode( + referenceNode, self.labelMapVolumeNode, slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY + ) elif isinstance(referenceNode, slicer.vtkMRMLLabelMapVolumeNode): labelMapVolumeNode = slicer.mrmlScene.CopyNode(referenceNode) self._labelMapVolumeNodeId = labelMapVolumeNode.GetID() @@ -151,10 +164,12 @@ def __process(self): if self._name.replace(" ", ""): labelMapVolumeNode.SetName(self._name) + labelMapVolumeNode.Modified() + try: - dataArray = np.array(slicer.util.arrayFromVolume(labelMapVolumeNode), copy=True, dtype=np.uint8) + array = slicer.util.arrayFromVolume(labelMapVolumeNode) + dataArray = np.array(array, copy=True, dtype=np.uint8) proportionDataArray = np.sort(dataArray, axis=2) slicer.util.updateVolumeFromArray(labelMapVolumeNode, proportionDataArray) - except: - # If there is not data yet to be pulled from the volume + except Exception: # In case where the reference node data is empty: pass diff --git a/src/ltrace/ltrace/utils/__init__.py b/src/ltrace/ltrace/utils/__init__.py index e69de29..a7ffe2a 100644 --- a/src/ltrace/ltrace/utils/__init__.py +++ b/src/ltrace/ltrace/utils/__init__.py @@ -0,0 +1 @@ +from .blob2hash import compute_md5 diff --git a/src/ltrace/ltrace/utils/blob2hash.py b/src/ltrace/ltrace/utils/blob2hash.py new file mode 100644 index 0000000..893091d --- /dev/null +++ b/src/ltrace/ltrace/utils/blob2hash.py @@ -0,0 +1,9 @@ +import hashlib + + +def compute_md5(file_name): + hash_md5 = hashlib.md5() + with open(file_name, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() diff --git a/src/ltrace/ltrace/workflow/Workflow.py b/src/ltrace/ltrace/workflow/Workflow.py index 4214f44..85336fe 100644 --- a/src/ltrace/ltrace/workflow/Workflow.py +++ b/src/ltrace/ltrace/workflow/Workflow.py @@ -31,6 +31,10 @@ PoreNetworkExtractor, PoreNetworkSimOnePhase, PoreNetworkSimTwoPhase, + PoreNetworkSimMercury, + ThinSectionPores, + ExportLAS, + InspectorIslands, ) } WORKSTEPS = collections.OrderedDict(sorted(WORKSTEPS.items())) # Sorting by name @@ -488,6 +492,7 @@ def workflowPanel(self): addCloneDeleteWorkstepsButtonsLayout = qt.QHBoxLayout() addCloneDeleteWorkstepsButtonsLayout.addWidget(qt.QWidget()) addWorkstepButton = qt.QPushButton("Add workstep") + addWorkstepButton.setObjectName("addWorkstepButton") addWorkstepButton.setAutoDefault(False) addWorkstepButton.setIcon(qt.QIcon(str(Customizer.ADD_ICON_PATH))) addCloneDeleteWorkstepsButtonsLayout.addWidget(addWorkstepButton) @@ -564,6 +569,7 @@ def onAddWorkstep(self): buttonsLayout = qt.QHBoxLayout() buttonsLayout.addStretch(1) okButton = qt.QPushButton("OK") + okButton.setObjectName("addWorkstepOkButton") okButton.setMinimumWidth(120) buttonsLayout.addWidget(okButton) cancelButton = qt.QPushButton("Cancel") diff --git a/src/ltrace/ltrace/workflow/workstep/data/ExportLAS.py b/src/ltrace/ltrace/workflow/workstep/data/ExportLAS.py new file mode 100644 index 0000000..1a0e0b3 --- /dev/null +++ b/src/ltrace/ltrace/workflow/workstep/data/ExportLAS.py @@ -0,0 +1,111 @@ +import os +import re +import ctk +import qt +import slicer +import lasio +import pandas as pd +from datetime import datetime +from ltrace.workflow.workstep import Workstep, WorkstepWidget +from ltrace.algorithms.measurements import LabelStatistics2D + + +class ExportLAS(Workstep): + NAME = "Data: Export LAS" + + INPUT_TYPES = (slicer.vtkMRMLLabelMapVolumeNode,) + OUTPUT_TYPE = type(None) + + def __init__(self): + super().__init__() + + def defaultValues(self): + self.exportPath = os.path.abspath(datetime.now().strftime("%Y%m%d_%H%M%S.las")) + + def run(self, nodes): + def export_las(df): + def split_param_and_unit(curve_name): + param = curve_name + unit_pattern = r"\((.*?)\)" + unit_match = re.search(unit_pattern, curve_name) + if unit_match: + unit = unit_match.group(1) + param = param.replace(f"({unit})", "").strip() + return param, unit + return param, "" + + las = lasio.LASFile() + las.well.DATE = datetime.today().strftime("%Y-%m-%d %H:%M:%S") + las.other = "Generated by GeoSlicer" + + df = df.sort_values(by="depth") + depths = df["depth"].copy() + df = df.drop("depth", axis=1) + + las.append_curve("DEPT", depths, unit="m") + + for name, curve in df.items(): + param, unit = split_param_and_unit(name) + las.append_curve(param, curve.to_numpy(), unit=unit) + + las.write(self.exportPath, version=2) + + grouped_data = pd.DataFrame([]) + unnecessary_attrs = ["label", "voxelCount", "pore_size_class"] + empty_depths = [] + + for node in nodes: + depth = float(node.GetName().split("_")[1].replace("-2", "")[:-1].replace(",", ".")) + reportID = node.GetAttribute("ResultReport") + if reportID: + reportNode = slicer.mrmlScene.GetNodeByID(node.GetAttribute("ResultReport")) + df = slicer.util.dataframeFromTable(reportNode) + df = df.drop(unnecessary_attrs, axis=1).mean() + df["depth"] = depth + + grouped_data = pd.concat([grouped_data, df.to_frame().T], ignore_index=True) + else: + empty_depths.append(depth) + + yield node + + if len(empty_depths) > 0: + if not grouped_data.empty: + df_columns = grouped_data.columns + else: + df_columns = [attr for attr in LabelStatistics2D.ATTRIBUTES if attr not in unnecessary_attrs] + [ + "depth" + ] + empty_data = pd.DataFrame(0, index=range(len(empty_depths)), columns=df_columns) + empty_data["depth"] = empty_depths + grouped_data = pd.concat([grouped_data, empty_data], ignore_index=True) + + export_las(grouped_data[["depth"] + list(grouped_data.columns)[:-1]]) + + def widget(self): + return ExportLASWidget(self) + + +class ExportLASWidget(WorkstepWidget): + def __init__(self, workstep): + WorkstepWidget.__init__(self, workstep) + + def setup(self): + WorkstepWidget.setup(self) + + self.formLayout = qt.QFormLayout() + self.formLayout.setLabelAlignment(qt.Qt.AlignRight) + self.layout().addLayout(self.formLayout) + + self.exportPathFileSelector = ctk.ctkPathLineEdit() + self.exportPathFileSelector.filters = ctk.ctkPathLineEdit.Files | ctk.ctkPathLineEdit.Writable + self.exportPathFileSelector.nameFilters = ["LAS files (*.las)"] + self.exportPathFileSelector.setCurrentPath("") + + self.formLayout.addRow("Export path:", self.exportPathFileSelector) + + def save(self): + self.workstep.exportPath = self.exportPathFileSelector.currentPath + + def load(self): + self.exportPathFileSelector.currentPath = self.workstep.exportPath diff --git a/src/ltrace/ltrace/workflow/workstep/data/ThinSectionLoader.py b/src/ltrace/ltrace/workflow/workstep/data/ThinSectionLoader.py index 726ca76..34a6fca 100644 --- a/src/ltrace/ltrace/workflow/workstep/data/ThinSectionLoader.py +++ b/src/ltrace/ltrace/workflow/workstep/data/ThinSectionLoader.py @@ -31,7 +31,7 @@ def run(self, nodes): ) logic = tsl.ThinSectionLoaderLogic() logic.load(params, baseName) - yield logic.node + yield slicer.mrmlScene.GetNodeByID(logic.nodeId) def expected_length(self, input_length): return len(list(self.files_to_load())) diff --git a/src/ltrace/ltrace/workflow/workstep/data/__init__.py b/src/ltrace/ltrace/workflow/workstep/data/__init__.py index fbeb384..973d0ab 100644 --- a/src/ltrace/ltrace/workflow/workstep/data/__init__.py +++ b/src/ltrace/ltrace/workflow/workstep/data/__init__.py @@ -2,3 +2,4 @@ from ltrace.workflow.workstep.data.Move import Move from ltrace.workflow.workstep.data.ThinSectionLoader import ThinSectionLoader from ltrace.workflow.workstep.data.NetCDFLoader import NetCDFLoader +from ltrace.workflow.workstep.data.ExportLAS import ExportLAS diff --git a/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorIslands.py b/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorIslands.py new file mode 100644 index 0000000..934fef3 --- /dev/null +++ b/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorIslands.py @@ -0,0 +1,92 @@ +from ltrace.slicer.ui import intParam +from ltrace.slicer.helpers import getSourceVolume +import qt +import slicer +from ltrace.workflow.workstep import Workstep, WorkstepWidget +from SegmentInspector import IslandsSettingsWidget, SegmentInspectorLogic + + +class InspectorIslands(Workstep): + NAME = "Segmentation: Islands from Segment Inspector" + + INPUT_TYPES = (slicer.vtkMRMLSegmentationNode, slicer.vtkMRMLLabelMapVolumeNode) + OUTPUT_TYPE = slicer.vtkMRMLLabelMapVolumeNode + + def __init__(self): + super().__init__() + + def defaultValues(self): + self.params = { + "method": IslandsSettingsWidget.METHOD, + "size_min_threshold": 100, + "direction": None, + } + + def run(self, segmentation_nodes): + queue = [] + logic = SegmentInspectorLogic(results_queue=queue) + + def run_islands(node): + def get_label_index(node): + segmentation = node.GetSegmentation() + + for i in range(segmentation.GetNumberOfSegments()): + if segmentation.GetNthSegment(i).GetLabelValue() == 1: + return i + raise ValueError("Could not find an appropriate segment to be used as input.") + + master_volume = getSourceVolume(node) if isinstance(node, slicer.vtkMRMLSegmentationNode) else node + if master_volume is None: + raise RuntimeError(f"No master volume found for segmentation node {node.GetName()}") + + logic.runSelectedMethod( + node, + segments=[get_label_index(node)], + outputPrefix=node.GetName() + "_{type}", + referenceNode=master_volume, + soiNode=None, + params=self.params, + products=["all"], + wait=True, + ) + slicer.app.processEvents() + + if not segmentation_nodes: + return + + for segmentation_node in segmentation_nodes: + run_islands(segmentation_node) + result = queue.pop(0) + + if not result: + continue + node = slicer.mrmlScene.GetNodeByID(result.outputVolume) + if node: + yield node + + def widget(self): + return InspectorIslandsWidget(self) + + +class InspectorIslandsWidget(WorkstepWidget): + def __init__(self, workstep): + WorkstepWidget.__init__(self, workstep) + + def setup(self): + WorkstepWidget.setup(self) + self.islands_widget = IslandsSettingsWidget() + self.layout().addWidget(self.islands_widget) + + self.formLayout = qt.QFormLayout() + self.formLayout.setLabelAlignment(qt.Qt.AlignRight) + self.layout().addLayout(self.formLayout) + + def save(self): + params = self.islands_widget.toJson() + params["direction"] = params["direction"].GetID() if params["direction"] else None + self.workstep.params = params + + def load(self): + params = self.workstep.params.copy() + params["direction"] = slicer.mrmlScene.GetNodeByID(params["direction"]) if params["direction"] else None + self.islands_widget.fromJson(params) diff --git a/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorWatershed.py b/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorWatershed.py index 1b1070d..851f931 100644 --- a/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorWatershed.py +++ b/src/ltrace/ltrace/workflow/workstep/segmentation/InspectorWatershed.py @@ -30,17 +30,26 @@ def run(self, segmentation_nodes): logic = SegmentInspectorLogic(results_queue=queue) def run_watershed(node): + def get_label_index(node): + segmentation = node.GetSegmentation() + + for i in range(segmentation.GetNumberOfSegments()): + if segmentation.GetNthSegment(i).GetLabelValue() == 1: + return i + raise ValueError("Could not find an appropriate segment to be used as input.") + master_volume = getSourceVolume(node) if isinstance(node, slicer.vtkMRMLSegmentationNode) else node if master_volume is None: raise RuntimeError(f"No master volume found for segmentation node {node.GetName()}") logic.runSelectedMethod( node, - segments=None, + segments=[get_label_index(node)], outputPrefix=node.GetName() + "_{type}", referenceNode=master_volume, soiNode=None, params=self.params, + products=["all"], wait=True, ) slicer.app.processEvents() diff --git a/src/ltrace/ltrace/workflow/workstep/segmentation/ThinSectionPores.py b/src/ltrace/ltrace/workflow/workstep/segmentation/ThinSectionPores.py new file mode 100644 index 0000000..7d68cc2 --- /dev/null +++ b/src/ltrace/ltrace/workflow/workstep/segmentation/ThinSectionPores.py @@ -0,0 +1,55 @@ +import os +import slicer +from ltrace.workflow.workstep import Workstep, WorkstepWidget +from ltrace.assets_utils import get_asset +from ltrace.slicer.tests.utils import wait_cli_to_finish +from Segmenter import MonaiModelsLogic +from types import SimpleNamespace + + +class ThinSectionPores(Workstep): + NAME = "Segmentation: Thin Section Pores" + + INPUT_TYPES = (slicer.vtkMRMLVectorVolumeNode,) + OUTPUT_TYPE = slicer.vtkMRMLSegmentationNode + + def __init__(self): + super().__init__() + + def defaultValues(self): + return + + def run(self, nodes): + inputModel = {"currentData": os.path.join(get_asset("ThinSectionEnv"), "carb_pore")} + + for node in nodes: + logic = MonaiModelsLogic(imageLogMode=False, onFinish=None) + cliNode = logic.run( + inputModelComboBox=SimpleNamespace(**inputModel), + referenceNode=node, + extraNodes=[], + soiNode=None, + outputPrefix=node.GetName() + "_{type}", + deterministic=False, + ) + + wait_cli_to_finish(cliNode) + + yield slicer.util.getNode(node.GetName() + "_Segmentation") + + def widget(self): + return ThinSectionPoresWidget(self) + + +class ThinSectionPoresWidget(WorkstepWidget): + def __init__(self, workstep): + WorkstepWidget.__init__(self, workstep) + + def setup(self): + WorkstepWidget.setup(self) + + def save(self): + return + + def load(self): + return diff --git a/src/ltrace/ltrace/workflow/workstep/segmentation/__init__.py b/src/ltrace/ltrace/workflow/workstep/segmentation/__init__.py index 3d63a26..2b09864 100644 --- a/src/ltrace/ltrace/workflow/workstep/segmentation/__init__.py +++ b/src/ltrace/ltrace/workflow/workstep/segmentation/__init__.py @@ -6,3 +6,5 @@ from ltrace.workflow.workstep.segmentation.Threshold import * from ltrace.workflow.workstep.segmentation.Watershed import * from ltrace.workflow.workstep.segmentation.InspectorWatershed import * +from ltrace.workflow.workstep.segmentation.ThinSectionPores import * +from ltrace.workflow.workstep.segmentation.InspectorIslands import * diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/InputTablesListWidget.py b/src/ltrace/ltrace/workflow/workstep/simulation/InputTablesListWidget.py new file mode 100644 index 0000000..e07030f --- /dev/null +++ b/src/ltrace/ltrace/workflow/workstep/simulation/InputTablesListWidget.py @@ -0,0 +1,161 @@ +import qt +import slicer + +from MercurySimulationLib.MercurySimulationLogic import ( + MercurySimulationLogic, + FixedRadiusLogic, + LeverettNewLogic, + LeverettOldLogic, + PressureCurveLogic, +) +from MercurySimulationLib.SubscaleModelWidget import ( + SubscaleModelWidget, + FixedRadiusWidget, + LeverettNewWidget, + LeverettOldWidget, + PressureCurveWidget, + ThroatRadiusCurveWidget, +) +from ltrace.slicer_utils import dataFrameToTableNode, dataframeFromTable +from ltrace.pore_networks.functions import geo2spy + +import numpy as np +import pandas as pd + +logic_models = { + FixedRadiusWidget.STR: FixedRadiusLogic(), + LeverettNewWidget.STR: LeverettNewLogic(), + LeverettOldWidget.STR: LeverettOldLogic(), + PressureCurveWidget.STR: PressureCurveLogic(), + ThroatRadiusCurveWidget.STR: PressureCurveLogic(), +} + + +def set_subres_model_and_params(table_node, idx, params, pressure_tables): + pore_network = geo2spy(table_node) + x_size = float(table_node.GetAttribute("x_size")) + y_size = float(table_node.GetAttribute("y_size")) + z_size = float(table_node.GetAttribute("z_size")) + volume = x_size * y_size * z_size + + subres_model = params["subres_model_name"] + subres_params = params["subres_params"] + + if subres_model == "Pressure Curve" or subres_model == "Throat Radius Curve": + subres_params = set_pressure_table_model(pressure_tables, subres_model, subres_params, idx) + + subresolution_function = logic_models[subres_model].get_capillary_pressure_function( + subres_params, pore_network, volume + ) + + return subresolution_function + + +def set_pressure_table_model(pressure_tables, subres_model, subres_params, idx): + pressure_table_len = len(pressure_tables) + if pressure_table_len != 0: + pressure_table_idx = pressure_table_len - 1 - idx + if pressure_table_len - 1 - idx < 0: + pressure_table_idx = 0 + print( + "Using the last curve from the list, as the number of pressure curves is less than number of selected pore nodes." + ) + pressure_curve_name, fvol_column, curve_column = pressure_tables[pressure_table_idx] + pressure_curve_node = slicer.util.getNode(pressure_curve_name) + df = dataframeFromTable(pressure_curve_node) + df = df.replace("", np.nan) + df = df.astype("float32") + Curve = df[curve_column].to_numpy() + Fvol = df[fvol_column].to_numpy() + + if subres_model == "Pressure Curve": + subres_params = {"throat radii": None, "capillary pressure": Curve, "dsn": Fvol} + elif subres_model == "Throat Radius Curve": + subres_params = {"throat radii": Curve, "capillary pressure": None, "dsn": Fvol} + else: + print("The table of pressures is empty. Executing the simulation with the currently selected options.") + subres_params = {i: np.asarray(subres_params[i]) for i in subres_params.keys()} + + return subres_params + + +class InputTablesListWidget(qt.QWidget): + def __init__(self, subscale_widget): + super().__init__() + layout = qt.QFormLayout(self) + + self.subscale_widget = subscale_widget + + self.queueLabel = qt.QLabel( + "Each pressure table listed below will be combined with a selected pore table\non the left, if they don't match your results may not be the expected." + ) + layout.addWidget(self.queueLabel) + + hboxLayout = qt.QHBoxLayout() + self.addButton = qt.QPushButton("Add to queue") + hboxLayout.addWidget(self.addButton) + self.removeButton = qt.QPushButton("Remove from queue") + hboxLayout.addWidget(self.removeButton) + layout.addRow("", hboxLayout) + self.addButton.connect("clicked(bool)", self.add) + self.removeButton.connect("clicked(bool)", self.remove) + + self.queue = qt.QTableWidget() + self.queue.horizontalHeader().setMinimumSectionSize(200) + self.queue.horizontalHeader().setStretchLastSection(qt.QHeaderView.Stretch) + self.queue.horizontalHeader().hide() + self.queue.verticalHeader().hide() + layout.addWidget(self.queue) + + subscale_widget.microscale_model_dropdown.currentTextChanged.connect(self.onChangeModel) + self.onChangeModel(subscale_widget.microscale_model_dropdown.currentText) + + def onChangeModel(self, text): + self.curve_widget = self.subscale_widget.parameter_widgets[text] + + state = text == "Pressure Curve" or text == "Throat Radius Curve" + self.setVisible(state) + self.queue.setRowCount(0) + + def setItem(self, row, col, string): + item = qt.QTableWidgetItem(string) + item.setTextAlignment(qt.Qt.AlignCenter) + item.setFlags(qt.Qt.ItemIsEnabled) + self.queue.setItem(row, col, item) + + def add(self): + row = self.queue.rowCount + self.queue.insertRow(row) + self.queue.setColumnCount(3) + + text = self.subscale_widget.microscale_model_dropdown.currentText + if text == "Pressure Curve": + node = self.curve_widget.pressureCurveSelector.currentNode() + elif text == "Throat Radius Curve": + node = self.curve_widget.throatRadiusSelector.currentNode() + + if node: + self.setItem(row, 0, node.GetName()) + self.setItem(row, 1, self.curve_widget.cboxes["Volume Fraction Column"].currentText) + if text == "Pressure Curve": + self.setItem(row, 2, self.curve_widget.cboxes["Throat Pressure Column"].currentText) + elif text == "Throat Radius Curve": + self.setItem(row, 2, self.curve_widget.cboxes["Throat Radius Column"].currentText) + + def remove(self): + row = self.queue.currentRow() + self.queue.removeRow(row) + + def write_qtable_to_list(self, table): + col_count = table.columnCount + row_count = table.rowCount + + list1 = [] + for row in range(row_count): + list2 = [] + for col in range(col_count): + table_item = table.item(row, col) + list2.append("" if table_item is None else str(table_item.text())) + list1.append(list2) + + return list1 diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkExtractor.py b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkExtractor.py index 79f55a0..270b536 100644 --- a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkExtractor.py +++ b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkExtractor.py @@ -1,14 +1,16 @@ import qt import slicer +import time from PoreNetworkExtractor import PoreNetworkExtractorLogic, PoreNetworkExtractorParamsWidget, PoreNetworkExtractorError from ltrace.workflow.workstep import Workstep, WorkstepWidget +from ltrace.slicer.widget.global_progress_bar import LocalProgressBar class PoreNetworkExtractor(Workstep): NAME = "Simulation: Pore Network Extraction" - INPUT_TYPES = (slicer.vtkMRMLLabelMapVolumeNode,) + INPUT_TYPES = (slicer.vtkMRMLScalarVolumeNode,) OUTPUT_TYPE = slicer.vtkMRMLTableNode def __init__(self): @@ -18,19 +20,29 @@ def defaultValues(self): self.method = "PoreSpy" self.delete_inputs = True - def run(self, label_map_nodes): - logic = PoreNetworkExtractorLogic() + def run(self, nodes): + progressBar = LocalProgressBar() + logic = PoreNetworkExtractorLogic(progressBar) - for label_map_node in label_map_nodes: + for node in nodes: + self.finished = False try: - extract_result = logic.extract(label_map_node, label_map_node.GetName(), self.method) + logic.extract(node, None, node.GetName(), self.method, self.onFinish) except PoreNetworkExtractorError: continue - finally: - self.discard_input(label_map_node) - pore_table, throat_table = extract_result + + while self.finished is False: + time.sleep(0.2) + slicer.app.processEvents() + + self.discard_input(node) + + pore_table, throat_table = logic.results["pore_table"], logic.results["throat_table"] yield pore_table + def onFinish(self, state): + self.finished = state + def widget(self): return PoreNetworkExtractorWidget(self) diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimMercury.py b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimMercury.py new file mode 100644 index 0000000..fb448bd --- /dev/null +++ b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimMercury.py @@ -0,0 +1,81 @@ +import numpy as np +import pandas as pd +import slicer +import qt + +import time + +from MercurySimulationLib.MercurySimulationLogic import MercurySimulationLogic +from MercurySimulationLib.MercurySimulationLogic import estimate_radius +from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget +from MercurySimulationLib.SubscaleModelWidget import SubscaleModelWidget + +from ltrace.slicer_utils import dataFrameToTableNode, dataframeFromTable +from ltrace.workflow.workstep import Workstep, WorkstepWidget +from ltrace.slicer.widget.global_progress_bar import LocalProgressBar +from .InputTablesListWidget import InputTablesListWidget, set_subres_model_and_params + + +class PoreNetworkSimMercury(Workstep): + NAME = "Simulation: Pore Network Simulation (Mercury)" + + INPUT_TYPES = (slicer.vtkMRMLTableNode,) + OUTPUT_TYPE = slicer.vtkMRMLTableNode + + def __init__(self): + super().__init__() + + def defaultValues(self): + self.params = MercurySimulationWidget.DEFAULT_VALUES + self.pressure_tables = [] + + def run(self, table_nodes): + progressBar = LocalProgressBar() + logic = MercurySimulationLogic(progressBar) + + for idx, pore_node in enumerate(table_nodes): + self.finished = False + + params = self.params.copy() + params["subresolution function call"] = lambda node: set_subres_model_and_params( + node, idx, params, self.pressure_tables + ) + params["subresolution function"] = params["subresolution function call"](pore_node) + + logic.run_mercury(pore_node, params, pore_node.GetName(), self.onFinish) + + while self.finished is False: + time.sleep(0.2) + slicer.app.processEvents() + + micp_table = slicer.util.getNode(logic.results_node_id) + + yield micp_table + + def onFinish(self, state): + self.finished = state + + def widget(self): + return PoreNetworkSimMercuryWidget(self) + + +class PoreNetworkSimMercuryWidget(WorkstepWidget): + def __init__(self, workstep): + WorkstepWidget.__init__(self, workstep) + + def setup(self): + WorkstepWidget.setup(self) + self.params_widget = SubscaleModelWidget() + self.layout().addWidget(self.params_widget) + + self.queue_widget = InputTablesListWidget(self.params_widget) + self.layout().addWidget(self.queue_widget) + + self.layout().addStretch(1) + + def save(self): + self.workstep.params.update(self.params_widget.getParams()) + self.workstep.pressure_tables = self.queue_widget.write_qtable_to_list(self.queue_widget.queue) + + def load(self): + self.params_widget.setParams(self.workstep.params) diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimOnePhase.py b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimOnePhase.py index d0288da..d22d262 100644 --- a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimOnePhase.py +++ b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimOnePhase.py @@ -2,10 +2,17 @@ import qt import numpy as np import pandas as pd -from PoreNetworkSimulation import PoreNetworkSimulationLogic +import time + +from PoreNetworkSimulation import OnePhaseSimulationLogic from PoreNetworkSimulation import OnePhaseSimulationWidget +from PoreNetworkSimulationLib.constants import MICP, ONE_PHASE, TWO_PHASE, ONE_ANGLE, MULTI_ANGLE + from ltrace.workflow.workstep import Workstep, WorkstepWidget from ltrace.slicer_utils import dataFrameToTableNode +from ltrace.slicer.widget.global_progress_bar import LocalProgressBar + +from .InputTablesListWidget import InputTablesListWidget, set_subres_model_and_params class PoreNetworkSimOnePhase(Workstep): @@ -19,15 +26,31 @@ def __init__(self): def defaultValues(self): self.params = OnePhaseSimulationWidget.DEFAULT_VALUES + self.pressure_tables = [] self.compiled_table_name = "Permeability results" def run(self, table_nodes): - logic = PoreNetworkSimulationLogic() + progressBar = LocalProgressBar() + logic = OnePhaseSimulationLogic(progressBar) folderTree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) results = {"Name": [], "x [mD]": [], "y [mD]": [], "z [mD]": []} - for table_node in table_nodes: - perm_table = logic.run_1phase(table_node, self.params["model type"], table_node.GetName()) + for idx, table_node in enumerate(table_nodes): + self.finished = False + + params = self.params.copy() + params["subresolution function call"] = lambda node: set_subres_model_and_params( + node, idx, params, self.pressure_tables + ) + params["subresolution function"] = params["subresolution function call"](table_node) + + logic.run_1phase(table_node, params, prefix=table_node.GetName(), callback=self.onFinish) + + while self.finished is False: + time.sleep(0.2) + slicer.app.processEvents() + + perm_table = slicer.util.getNode(logic.results["permeability"]) perm_table.SetName(table_node.GetName() + " perm") itemTreeId = folderTree.GetItemByDataNode(perm_table) parentItemId = folderTree.GetItemParent(folderTree.GetItemParent(itemTreeId)) @@ -45,6 +68,9 @@ def run(self, table_nodes): results_table = dataFrameToTableNode(pd.DataFrame(results)) results_table.SetName(self.compiled_table_name) + def onFinish(self, state): + self.finished = state + def widget(self): return PoreNetworkSimOnePhaseWidget(self) @@ -55,19 +81,26 @@ def __init__(self, workstep): def setup(self): WorkstepWidget.setup(self) - self.params_widget = OnePhaseSimulationWidget() - self.compiled_table_edit = qt.QLineEdit() - form_layout = qt.QFormLayout() + + self.params_widget = OnePhaseSimulationWidget() + self.params_widget.mercury_widget.micpCollapsibleButton.setVisible(False) form_layout.addRow(self.params_widget) + + self.queue_widget = InputTablesListWidget(self.params_widget.mercury_widget.subscaleModelWidget) + form_layout.addRow(self.queue_widget) + + self.compiled_table_edit = qt.QLineEdit() form_layout.addRow("Compiled permeability table name:", self.compiled_table_edit) self.layout().addLayout(form_layout) self.layout().addStretch(1) def save(self): - self.workstep.params = self.params_widget.getParams() + self.workstep.params.update(self.params_widget.getParams()) + del self.workstep.params["subresolution function call"] self.workstep.compiled_table_name = self.compiled_table_edit.text + self.workstep.pressure_tables = self.queue_widget.write_qtable_to_list(self.queue_widget.queue) def load(self): self.params_widget.setParams(self.workstep.params) diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimTwoPhase.py b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimTwoPhase.py index 987ed4d..d19ab7e 100644 --- a/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimTwoPhase.py +++ b/src/ltrace/ltrace/workflow/workstep/simulation/PoreNetworkSimTwoPhase.py @@ -1,26 +1,37 @@ import slicer -from PoreNetworkSimulation import PoreNetworkSimulationLogic +from PoreNetworkSimulation import TwoPhaseSimulationLogic from PoreNetworkSimulation import TwoPhaseSimulationWidget from ltrace.workflow.workstep import Workstep, WorkstepWidget +from ltrace.slicer.widget.global_progress_bar import LocalProgressBar + +from .InputTablesListWidget import InputTablesListWidget, set_subres_model_and_params class PoreNetworkSimTwoPhase(Workstep): NAME = "Simulation: Pore Network Simulation (Two-phase)" INPUT_TYPES = (slicer.vtkMRMLTableNode,) - OUTPUT_TYPE = type(None) + OUTPUT_TYPE = slicer.vtkMRMLTableNode def __init__(self): super().__init__() def defaultValues(self): self.params = TwoPhaseSimulationWidget.DEFAULT_VALUES + self.pressure_tables = [] def run(self, table_nodes): - logic = PoreNetworkSimulationLogic() + progressBar = LocalProgressBar() + logic = TwoPhaseSimulationLogic(progressBar) + + for idx, table_node in enumerate(table_nodes): + params = self.params.copy() + params["subresolution function call"] = lambda node: set_subres_model_and_params( + node, idx, params, self.pressure_tables + ) + params["subresolution function"] = params["subresolution function call"](table_node) - for table_node in table_nodes: - logic.run_2phase(table_node, self.params, table_node.GetName()) + logic.run_2phase(table_node, params, prefix=table_node.GetName(), callback=lambda _: True, wait=True) yield f"two-phase simulation of {table_node.GetName()}" def widget(self): @@ -33,12 +44,20 @@ def __init__(self, workstep): def setup(self): WorkstepWidget.setup(self) + self.params_widget = TwoPhaseSimulationWidget() + self.params_widget.mercury_widget.micpCollapsibleButton.setVisible(False) self.layout().addWidget(self.params_widget) + + self.queue_widget = InputTablesListWidget(self.params_widget.mercury_widget.subscaleModelWidget) + self.layout().addWidget(self.queue_widget) + self.layout().addStretch(1) def save(self): self.workstep.params = self.params_widget.getParams() + del self.workstep.params["subresolution function call"] + self.workstep.pressure_tables = self.queue_widget.write_qtable_to_list(self.queue_widget.queue) def load(self): self.params_widget.setParams(self.workstep.params) diff --git a/src/ltrace/ltrace/workflow/workstep/simulation/__init__.py b/src/ltrace/ltrace/workflow/workstep/simulation/__init__.py index 0e80daf..15b3bf1 100644 --- a/src/ltrace/ltrace/workflow/workstep/simulation/__init__.py +++ b/src/ltrace/ltrace/workflow/workstep/simulation/__init__.py @@ -1,3 +1,4 @@ from .PoreNetworkExtractor import * from .PoreNetworkSimOnePhase import * from .PoreNetworkSimTwoPhase import * +from .PoreNetworkSimMercury import * diff --git a/src/ltrace/ltrace/wrappers.py b/src/ltrace/ltrace/wrappers.py index fb12440..6edaf69 100644 --- a/src/ltrace/ltrace/wrappers.py +++ b/src/ltrace/ltrace/wrappers.py @@ -1,12 +1,5 @@ -import os -import sys -import re - -from functools import wraps -from ltrace.constants import MAX_LOOP_ITERATIONS -from pathlib import Path from time import perf_counter -from typing import Union, Tuple, Optional +from functools import wraps def timeit(function): @@ -59,102 +52,3 @@ def _addAttributes(function): return function return _addAttributes - - -def filter_module_name(module_name: str) -> str: - return re.sub(r"[^a-zA-Z0-9\.\:\_\- ]", "", module_name) - - -def filter_path_string(path_string: str) -> str: - return re.sub(r'[^a-zA-Z0-9.\/\\\:-_#@!%*()="\+\- ]', "", path_string) - - -def sanitize_file_path(path: Union[Path, str]) -> Path: - """Sanitize file path to avoid invalid characters and path traversal vulnerabilities. - - Args: - path (Union[Path, str]): The path to be sanitized. - - Returns: - Path: The sanitized path as a Path object - - Raise: - ValueError: If the selected path is located in a forbidden base path. - """ - if isinstance(path, str): - path = Path(filter_path_string(path)) - - if path is None or not path: - raise ValueError("The path's input is empty. Please select a valid path.") - - path: Path = path.resolve() - - allowed_base_paths = [] - prohibited_base_paths = [] - if sys.platform == "win32": - env_vars = dict(os.environ.items()) - systemDrive = env_vars.get("SYSTEMDRIVE", path.drive) - windowsFolder = env_vars.get("SYSTEMROOT", (Path(systemDrive) / "Windows").as_posix()) - appDataFolder = env_vars.get("APPDATA", (Path(systemDrive) / "Users").as_posix()) - programDataFolder = env_vars.get("PROGRAMDATA", (Path(systemDrive) / "ProgramData").as_posix()) - programFilesFolder = env_vars.get("PROGRAMFILES", (Path(systemDrive) / "ProgramFiles").as_posix()) - programFilesx86Folder = env_vars.get("PROGRAMFILES(x86)", (Path(systemDrive) / "ProgramFiles(x86)").as_posix()) - allowed_base_paths = [] # everywhere allowed, except for the prohibited folders - prohibited_base_paths = [ - appDataFolder, - programDataFolder, - windowsFolder, - programDataFolder, - programFilesFolder, - programFilesx86Folder, - # Adding hardcoded variations to avoid problems with internationalization - "C:/ProgramData", - "C:/Program Files", - "C:/Program Files (x86)", - ] - - else: - allowed_base_paths = [] # everywhere allowed, except for the prohibited folders - prohibited_base_paths = [ - "/bin", - "/boot", - "/cdrom", - "/dev", - "/etc", - "/lib", - "/lib32", - "/lib64", - "/libx32", - "/lost+found", - "/media", - "/opt", - "/proc", - "/run", - "/sbin", - "/snap", - "/srv", - "/swapfile", - "/sys", - "/usr", - "/var", - ] - allowed_base_paths = [filter_path_string(path) for path in allowed_base_paths] - prohibited_base_paths = [filter_path_string(path) for path in prohibited_base_paths] - - allowed = len(allowed_base_paths) == 0 - if not allowed: - for base_path in allowed_base_paths[:MAX_LOOP_ITERATIONS]: - if path.is_relative_to(base_path): - allowed = True - break - - if allowed: - for base_path in prohibited_base_paths[:MAX_LOOP_ITERATIONS]: - if path.is_relative_to(base_path): - allowed = False - break - - if not allowed: - raise ValueError(f"The base path for '{path.as_posix()}' is prohibited. Please select another location.") - - return path diff --git a/src/ltrace/requirements.txt b/src/ltrace/requirements.txt index 51359aa..dd49f2c 100644 --- a/src/ltrace/requirements.txt +++ b/src/ltrace/requirements.txt @@ -6,7 +6,7 @@ Cython==0.29.28 dask-image==0.4.0 dask[complete]==2.30.0 distinctipy==1.1.5 -dlisio==0.3.5 +dlisio==1.0.1 h5netcdf==1.1.0 h5py==3.6.0 h5pyd==0.14.1 @@ -15,7 +15,7 @@ Jinja2==2.11.1 joblib==1.1.1 keyring==24.2.0 keyrings.cryptfile==1.3.9 ; platform_system != "Windows" -lasio==0.25.1 +lasio @ git+https://github.com/kinverarity1/lasio@b472b1f loguru==0.6.0 markdown2==2.4.2 markupsafe==2.0.1 @@ -36,16 +36,17 @@ scikit-fmm==2022.3.26 # Workaround for failed wheels bulding on porespy package pypardiso==0.4.3 # Fix version because they changed the package metadata and defined an invalid string for licence openpnm==3.4.0 pandas==1.4.2 -paramiko==2.10.3 +paramiko==3.4.0 pathvalidate==2.5.0 pint==0.19.2 pint-pandas==0.2 psutil==5.9.0 +pyedt==0.1.4 pygments>=2.11.2 -pyqtgraph @ git+https://github.com/ltracegeo/pyqtgraph.git@master +pyqtgraph @ git+https://github.com/ltracegeo/pyqtgraph.git@pyqtgraph-0.12.4.2 PySide2==5.15.2 pytesseract==0.3.7 -pywin32==228 ; platform_system == "Windows" +pywin32==306 ; platform_system == "Windows" pyzmq==22.3.0 recordtype==1.3.0 sahi==0.11.15 @@ -65,5 +66,11 @@ xarray==2022.3.0 zarr==2.5.0 statsmodels== 0.14.0 detect_delimiter==0.1.1 -jsonschema==3.2.0 +stopit==1.1.2 +streamlit==1.22.0 +pynrrd==1.0.0 +plotly==5.18.0 +itkwidgets==0.32.6 +ipywidgets==7.6.3 +widgetsnbextension==3.5.2 drd==0.1.2 diff --git a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py index 231224e..c9c83b8 100644 --- a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py +++ b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py @@ -9,14 +9,14 @@ import slicer import vtk import qSlicerSegmentationsEditorEffectsPythonQt as effects +import traceback from ltrace.slicer import helpers from ltrace.slicer.lazy import lazy +from ltrace.slicer_utils import LTraceSegmentEditorEffectMixin from SegmentEditorEffects import * from typing import Union -from ltrace.slicer_utils import LTraceSegmentEditorEffectMixin - FILTER_GRADIENT_MAGNITUDE = "GRADIENT_MAGNITUDE" @@ -42,6 +42,7 @@ def __init__(self, scriptedEffect): self.previewStep = 1 self.previewSteps = 20 self.timer.connect("timeout()", self.preview) + self.timer.setParent(self.scriptedEffect.optionsFrame()) self.isInitialized = False @@ -99,8 +100,8 @@ def deactivate(self): if not self.keepFilterResultCheckBox.isChecked(): try: slicer.mrmlScene.RemoveNode(self.filterOutputVolume) - except: - pass + except Exception as error: + logging.debug(f"Error: {error}. Traceback:\n{traceback.format_exc()}") try: segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() @@ -115,8 +116,8 @@ def deactivate(self): segmentation.GetSegmentIDs(segmentIDs) for index in range(segmentIDs.GetNumberOfValues()): segmentationNode.GetDisplayNode().SetSegmentVisibility(segmentIDs.GetValue(index), True) - except: - pass + except Exception as error: + logging.debug(f"Error: {error}. Traceback:\n{traceback.format_exc()}") def setCurrentSegmentTransparent(self): """Save current segment opacity and set it to zero @@ -126,6 +127,10 @@ def setCurrentSegmentTransparent(self): Call restorePreviewedSegmentTransparency() to restore original opacity. """ + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -153,6 +158,10 @@ def setCurrentSegmentTransparent(self): def restorePreviewedSegmentTransparency(self): """Restore previewed segment's opacity that was temporarily made transparen by calling setCurrentSegmentTransparent().""" + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -264,7 +273,11 @@ def setEnabledSegmentationButtons(self, enabled: bool): addSegmentButton = editorWidget.findChild(qt.QPushButton, "AddSegmentButton") addSegmentButton.setEnabled(enabled) - hasSelectedSegment = bool(self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()) + hasSelectedSegment = ( + bool(self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()) + if self.scriptedEffect.parameterSetNode() is not None + else False + ) removeSegmentButton = editorWidget.findChild(qt.QPushButton, "RemoveSegmentButton") removeSegmentButton.setEnabled(enabled and hasSelectedSegment) @@ -321,8 +334,11 @@ def updateMRMLFromGUI(self): # Effect specific methods (the above ones are the API methods to override) # def initialize(self): - self.scriptedEffect.saveStateForUndo() + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to initialize the effect. The selected node is not valid.") + return + self.scriptedEffect.saveStateForUndo() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() segmentIDs = vtk.vtkStringArray() segmentation = segmentationNode.GetSegmentation() @@ -379,7 +395,7 @@ def initialize(self): # maxValue += step # array[array > maxValue] = maxValue # slicer.util.updateVolumeFromArray(self.filterOutputVolume, array) - + self.selectedSegment = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() self.invisibleSegments = self.removeSegmentationVisibleSegments() for index in range(segmentIDs.GetNumberOfValues()): @@ -433,6 +449,10 @@ def autoThreshold(self): # Returns a list of tuples containing the necessary parameters to add the segments back def removeSegmentationVisibleSegments(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return None + removedSegments = list() # Always affects the current scriptedEffect's segmentation node directly, as @@ -458,6 +478,10 @@ def removeSegmentationVisibleSegments(self): return removedSegments def onApplyAll(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + def getLazySegmentation(parentName: str) -> Union[None, slicer.vtkMRMLNode]: segmentationNode = None lazyNodes = slicer.util.getNodesByClass("vtkMRMLTextNode") @@ -515,8 +539,11 @@ def onApply(self): thresh.Update() modifierLabelmap.DeepCopy(thresh.GetOutput()) except IndexError: - logging.error("apply: Failed!") - pass + logging.error(f"Error: {error}") + except Exception as error: + logging.debug(f"Error: {error}. Traceback:\n{traceback.format_exc()}") + slicer.util.errorDisplay(f"Failed to apply the effect.\nError: {error}") + return # Apply changes self.scriptedEffect.modifySelectedSegmentByLabelmap( @@ -573,7 +600,9 @@ def onApply(self): slicer.mrmlScene.RemoveNode(self.filterOutputVolume) except: pass - + # restore segment selection enableling effects + if self.selectedSegment: + self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(self.selectedSegment) self.applyFinishedCallback() def clearPreviewDisplay(self): @@ -647,7 +676,8 @@ def preview(self): self.previewStep = -1 if self.previewState <= 0: self.previewStep = 1 - except: + except Exception as error: + logging.debug(f"Error {error}. Traceback:\n{traceback.format_exc()}") # When an undo action is performed, it causes and exception due the to the removed Edges segment. We deactivate to start fresh all over again self.deactivate() diff --git a/src/modules/Charts/Charts.py b/src/modules/Charts/Charts.py index bc531d2..04bf20c 100644 --- a/src/modules/Charts/Charts.py +++ b/src/modules/Charts/Charts.py @@ -4,14 +4,23 @@ import logging from ltrace.slicer.node_attributes import NodeEnvironment from ltrace.slicer_utils import * +from pathlib import Path from Plots.Crossplot.CrossplotWidget import CrossplotWidget from Plots.BarPlot.BarPlotBuilder import BarPlotBuilder from Plots.Windrose.WindrosePlotBuilder import WindrosePlotBuilder from Plots.Crossplot.CrossplotPlotBuilder import CrossplotBuilder from Plots.HistogramInDepthPlot.HistogramInDepthPlotBuilder import HistogramInDepthPlotBuilder from Plots.HistogramPlot.HistogramPlotBuilder import HistogramPlotBuilder +from DLISImportLib.DLISImportLogic import WELL_NAME_TAG +import numpy as np +from ltrace.slicer.helpers import createTemporaryNode, removeTemporaryNodes +from ltrace.slicer_utils import dataframeFromTable, dataFrameToTableNode + +try: + from Test.CrossplotWidgetTest import CrossplotWidgetTest +except ImportError: + CrossplotWidgetTest = None -from pathlib import Path INCOMPATIBLE_MESSAGE = ( "The input table cannot be plotted because its format is not compatible with the chosen chart type." @@ -53,16 +62,19 @@ def readme_path(cls): class ChartsWidget(LTracePluginWidget): CREATE_NEW_PLOT_LABEL = "Create new plot" - PLOT_TYPES = [ - CrossplotBuilder(), - WindrosePlotBuilder(), - BarPlotBuilder(), - HistogramInDepthPlotBuilder(), - HistogramPlotBuilder(), - ] RESOURCES_PATH = Path(__file__).absolute().with_name("Resources") WINDOWN_ICON = RESOURCES_PATH / "Icons" / "Charts.png" - __plotWidgets = dict() + + def __init__(self, parent) -> None: + super().__init__(parent) + self.__plotWidgetBuilders = [ + CrossplotBuilder(), + WindrosePlotBuilder(), + BarPlotBuilder(), + HistogramInDepthPlotBuilder(), + HistogramPlotBuilder(), + ] + self.__plotWidgets = dict() def setup(self): LTracePluginWidget.setup(self) @@ -74,11 +86,12 @@ def setup(self): # Table node list widget (Subject Hierarchy Tree View) self.nodeSelector = self._create_node_selector() parametersFormLayout.addRow(self.nodeSelector) + self.current_items_ids = vtk.vtkIdList() # Plot type combo box self.plotTypeComboBox = qt.QComboBox() - plot_type_labels = [plotWidgetBuilder.TYPE for plotWidgetBuilder in self.PLOT_TYPES] + plot_type_labels = [plotWidgetBuilder.TYPE for plotWidgetBuilder in self.__plotWidgetBuilders] self.plotTypeComboBox.addItems(plot_type_labels) parametersFormLayout.addRow("Plot type: ", self.plotTypeComboBox) @@ -92,7 +105,7 @@ def setup(self): # Add Stacked widget to add the selected plot configuration widgets self.plotConfigurationWidget = qt.QStackedWidget() - for plotWidgetBuilder in self.PLOT_TYPES: + for plotWidgetBuilder in self.__plotWidgetBuilders: widget = plotWidgetBuilder.configurationWidget() self.plotConfigurationWidget.addWidget(widget) @@ -116,7 +129,7 @@ def enter(self) -> None: super().enter() def exit(self): - pass + removeTemporaryNodes() def _create_node_selector(self): """Handles node selector widget creation. @@ -214,7 +227,7 @@ def __handle_plot_type_creation(self, plotType, plotLabel): plotType (str): the plot type string plotLabel (str): the plot label """ - for plotWidgetBuilder in self.PLOT_TYPES: + for plotWidgetBuilder in self.__plotWidgetBuilders: if plotType == plotWidgetBuilder.TYPE: plotWidget = plotWidgetBuilder.build(plotLabel=plotLabel, parent=None) @@ -271,7 +284,13 @@ def onPlotButtonClicked(self): """Handle the click event at the plot button.""" currentNodes = self._get_selected_nodes() - failures = [node.GetName() for node in currentNodes if isVarDescriptor(node)] + nodes = ChartsWidget.getNodesMergedByWell(currentNodes) + + self.plot(nodes) + + def plot(self, nodes): + + failures = [node.GetName() for node in nodes if isVarDescriptor(node)] if failures: message = ( @@ -298,7 +317,7 @@ def onPlotButtonClicked(self): if selectPlotWidget is None: return - for currentNode in currentNodes: + for currentNode in nodes: try: selectPlotWidget.appendData(currentNode) except (ValueError, RuntimeError) as error: @@ -320,7 +339,7 @@ def __onPlotTypeComboBoxChanged(self, text): self.__updateConfigurationWidget(text) def __updateConfigurationWidget(self, plotTypeLabel): - for plotWidgetBuilder in self.PLOT_TYPES: + for plotWidgetBuilder in self.__plotWidgetBuilders: if plotWidgetBuilder.TYPE != plotTypeLabel: continue @@ -350,6 +369,48 @@ def setSelectedNode(self, node): def _on_node_selector_item_changed(self, item_id): pass + def getNodesMergedByWell(nodes): + outNodes = [] + wellsIndexesNodes = [] # tables from wells need to be merged into a multi-colun table per well + wells = [] + for node in nodes: + wellName = node.GetAttribute(WELL_NAME_TAG) + wellsIndexesNodes.append( + { + "WellName": wellName, + "node": node, + } + ) + if wellName is None or wellName == "": + outNodes.append(node) + elif wellName not in wells: + wells.append(wellName) + + # + # If there are tables originated from wells, we merge them into a multi-column table per well + + nodesToMerge = [] + for w in wells: + if w is not None: + nodesToMerge.append([d["node"] for d in wellsIndexesNodes if d["WellName"] == w]) + + mergedTableNode = None + if len(nodesToMerge) > 0: + for w in range(len(nodesToMerge)): + df = dataframeFromTable( + nodesToMerge[w][0] + ) # table node to which the other ones of the same well will be appended + for i, node in enumerate(nodesToMerge[w][1:], start=1): + dfToAdd = dataframeFromTable(node) + columnToAdd = dfToAdd.values[0:, 1] + df.insert(df.values.shape[1], dfToAdd.columns[1], columnToAdd) + + mergedTableNode = createTemporaryNode(slicer.vtkMRMLTableNode, wells[w]) + dataFrameToTableNode(df, mergedTableNode) + outNodes.append(mergedTableNode) + + return outNodes + class ChartsLogic(LTracePluginLogic): def __init__(self): diff --git a/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py b/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py index e1305e1..e083f33 100644 --- a/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py +++ b/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py @@ -178,23 +178,26 @@ class CrossplotWidget(BasePlotWidget): def __init__(self, plotLabel="", *args, **kwargs): super().__init__(plotType=self.TYPE, plotLabel=plotLabel, *args, **kwargs) + self.dataPlotWidget = None self.__graphDataList = list() + self.__tableWidget = None self.__zMinValueRange = 0 self.__zMaxValueRange = 0 self.__createPlotUpdateTimer() - self.__fit_data_list = [] - self.__valid_fitted_curve_selected = False + self.__fitDataList = [] + self.__validFittedCurveSelected = False - self.__fit_equations = [Line(), TimurCoates()] + self.__fitEquations = [Line(), TimurCoates()] def setupUi(self): """Initialize widgets""" + self.setObjectName("Crossplot Widget") self.setMinimumSize(780, 600) layout = QtGui.QHBoxLayout() - parameters_widget = QtGui.QFrame() - parameters_layout = QtGui.QVBoxLayout() - parameters_widget.setLayout(parameters_layout) + parametersWidget = QtGui.QFrame() + parametersLayout = QtGui.QVBoxLayout() + parametersWidget.setLayout(parametersLayout) plot_layout = QtGui.QVBoxLayout() # Data table widget @@ -203,11 +206,12 @@ def setupUi(self): self.__tableWidget.signal_data_removed.connect(self.__removeDataFromTableByName) self.__tableWidget.signal_all_style_changed.connect(self.__updateAllDataStyles) self.__tableWidget.signal_all_visible_changed.connect(self.__updateAllDataVisibility) - parameters_layout.addWidget(self.__tableWidget) + parametersLayout.addWidget(self.__tableWidget) # Plot widget - self.data_plot_widget = DataPlotWidget() - plot_layout.addWidget(self.data_plot_widget.widget) + self.dataPlotWidget = DataPlotWidget() + self.dataPlotWidget.toggleLegendSignal.connect(self.__toggleLegend) + plot_layout.addWidget(self.dataPlotWidget.widget) plot_layout.addStretch() @@ -238,6 +242,7 @@ def setupUi(self): # Parameter combobox self.__xAxisComboBox = QtGui.QComboBox() + self.__xAxisComboBox.objectName = "X Axis Combo Box" xAxisParameterLayout = QtGui.QFormLayout() xAxisParameterLayout.addRow("Parameter", self.__xAxisComboBox) xAxisParameterLayout.setHorizontalSpacing(8) @@ -261,7 +266,7 @@ def setupUi(self): self.__xAxisGroupBox = QtGui.QGroupBox("X axis") self.__xAxisGroupBox.setLayout(self.__xAxisGridLayout) - parameters_layout.addWidget(self.__xAxisGroupBox) + parametersLayout.addWidget(self.__xAxisGroupBox) # Y axis # Histogram options @@ -290,6 +295,7 @@ def setupUi(self): # Parameter combobox self.__yAxisComboBox = QtGui.QComboBox() + self.__yAxisComboBox.objectName = "Y Axis Combo Box" yAxisParameterLayout = QtGui.QFormLayout() yAxisParameterLayout.addRow("Parameter", self.__yAxisComboBox) yAxisParameterLayout.setHorizontalSpacing(8) @@ -313,7 +319,7 @@ def setupUi(self): self.__yAxisGroupBox = QtGui.QGroupBox("Y axis") self.__yAxisGroupBox.setLayout(self.__yAxisGridLayout) - parameters_layout.addWidget(self.__yAxisGroupBox) + parametersLayout.addWidget(self.__yAxisGroupBox) # Z axis self.__zAxisComboBox = QtGui.QComboBox() @@ -358,46 +364,55 @@ def setupUi(self): zStyleGroupBox = QtGui.QGroupBox("Z axis") zStyleGroupBox.setLayout(formLayout) - parameters_layout.addWidget(zStyleGroupBox) + parametersLayout.addWidget(zStyleGroupBox) + + # Settings + self.__settingsGroupBox = QtGui.QGroupBox("Settings") - # Theme - self.__themeGroupBox = QtGui.QGroupBox("Theme settings") self.__themeComboBox = QtGui.QComboBox() - themeParameterLayout = QtGui.QFormLayout() - themeParameterLayout.setHorizontalSpacing(8) - themeParameterLayout.addRow("Theme", self.__themeComboBox) - for themeName in self.data_plot_widget.themes: + for themeName in self.dataPlotWidget.themes: self.__themeComboBox.addItem(themeName) - self.__themeGroupBox.setLayout(themeParameterLayout) - parameters_layout.addWidget(self.__themeGroupBox) - self.__themeComboBox.setCurrentText(self.data_plot_widget.themes[0]) + self.__embeddedLegendVisibilityCheckBox = QtGui.QCheckBox() + self.__embeddedLegendVisibilityCheckBox.setChecked(self.dataPlotWidget.embeddedLegendVisibility) + self.__embeddedLegendVisibilityCheckBox.stateChanged.connect(self.__onEmbeddedLegendVisibilityChange) + + settingsFormLayout = QtGui.QFormLayout() + settingsFormLayout.setHorizontalSpacing(8) + + settingsFormLayout.addRow("Theme", self.__themeComboBox) + settingsFormLayout.addRow("Show legend", self.__embeddedLegendVisibilityCheckBox) + self.__settingsGroupBox.setLayout(settingsFormLayout) + + parametersLayout.addWidget(self.__settingsGroupBox) + + self.__themeComboBox.setCurrentText(self.dataPlotWidget.themes[0]) # Stretch - parameters_layout.addStretch() + parametersLayout.addStretch() # Tabs - tab_widget = QtGui.QTabWidget() - tab_widget.addTab(parameters_widget, "Data") - fit_frame_qt = self.__create_fit_tab() - fit_frame_qtgui = shiboken2.wrapInstance(hash(fit_frame_qt), QtGui.QFrame) - tab_widget.addTab(fit_frame_qtgui, "Curve fitting") - self.equations_tab = EquationsTabWidget(self.__fit_data_list) - self.equations_tab.signal_new_function_curve_data.connect(self.__on_fit_data_created) - self.equations_tab.signal_import_function_curve.connect(self.__on_import_clicked) - self.equations_tab.signal_export_function_curve.connect(self.__on_export_clicked) - self.equations_tab.signal_function_curve_edited.connect(self.__on_function_curve_edited) - self.equations_tab.signal_save_data.connect(self.__on_function_curve_save_button_clicked) - equations_tab_qtgui = shiboken2.wrapInstance(hash(self.equations_tab), QtGui.QFrame) - tab_widget.addTab(equations_tab_qtgui, "Curves") - - shortest_width = min(parameters_widget.sizeHint().width(), fit_frame_qtgui.sizeHint().width()) - parameters_widget.setMaximumWidth(shortest_width) - fit_frame_qtgui.setMaximumWidth(shortest_width) - tab_widget.setMaximumWidth(shortest_width) + tabWidget = QtGui.QTabWidget() + tabWidget.addTab(parametersWidget, "Data") + fitFrameQt = self.__createFitTab() + fitFrameQtGui = shiboken2.wrapInstance(hash(fitFrameQt), QtGui.QFrame) + tabWidget.addTab(fitFrameQtGui, "Curve fitting") + self.equationsTab = EquationsTabWidget(self.__fitDataList) + self.equationsTab.signalNewFunctionCurveData.connect(self.__onFitDataCreated) + self.equationsTab.signalImportFunctionCurve.connect(self.__onImportClicked) + self.equationsTab.signalExportFunctionCurve.connect(self.__onExportClicked) + self.equationsTab.signalFunctionCurveEdited.connect(self.__onFunctionCurveEdited) + self.equationsTab.signalSaveData.connect(self.__on_function_curve_save_button_clicked) + equationsTabQtGui = shiboken2.wrapInstance(hash(self.equationsTab), QtGui.QFrame) + tabWidget.addTab(equationsTabQtGui, "Curves") + + shortest_width = min(parametersWidget.sizeHint().width(), fitFrameQtGui.sizeHint().width()) + parametersWidget.setMaximumWidth(shortest_width) + fitFrameQtGui.setMaximumWidth(shortest_width) + tabWidget.setMaximumWidth(shortest_width) # Layout - layout.addWidget(tab_widget) + layout.addWidget(tabWidget) layout.addLayout(plot_layout) layout.setSpacing(10) self.setLayout(layout) @@ -421,221 +436,227 @@ def setupUi(self): # Apply default values self.__autoRangeCheckBox.setChecked(True) - self.data_plot_widget.set_theme(self.__themeComboBox.currentText()) + self.dataPlotWidget.set_theme(self.__themeComboBox.currentText()) self.__updatePlotsLayout() - self.__on_fitted_curve_selected("") + self.__onFittedCurveSelected("") + + @property + def graphDataList(self): + return list(self.__graphDataList) - def __create_fit_tab(self): + def __createFitTab(self): ## New fit widget - self.__fit_data_input_combo_box = qt.QComboBox() + self.__fitDataInputComboBox = qt.QComboBox() - self.__fit_equation_combo_box = qt.QComboBox() - for fit_equation in self.__fit_equations: - self.__fit_equation_combo_box.addItem(fit_equation.widget.DISPLAY_NAME) - self.__fit_equation_combo_box.currentTextChanged.connect(self.__select_fit_equation) + self.__fitEquationComboBox = qt.QComboBox() + for fitEquation in self.__fitEquations: + self.__fitEquationComboBox.addItem(fitEquation.widget.DISPLAY_NAME) + self.__fitEquationComboBox.currentTextChanged.connect(self.__selectFitEquation) - fit_button = qt.QPushButton("New fit") - fit_button.setFocusPolicy(qt.Qt.NoFocus) - fit_button.clicked.connect(self.__on_fit_clicked) + fitButton = qt.QPushButton("New fit") + fitButton.setFocusPolicy(qt.Qt.NoFocus) + fitButton.clicked.connect(self.__onFitClicked) - fit_buttons_layout = qt.QHBoxLayout() - fit_buttons_layout.addWidget(fit_button) + fitButtonsLayout = qt.QHBoxLayout() + fitButtonsLayout.addWidget(fitButton) - fit_input_layout = qt.QFormLayout() - fit_input_layout.addRow("Data: ", self.__fit_data_input_combo_box) - fit_input_layout.addRow("Equation: ", self.__fit_equation_combo_box) - fit_input_layout.addRow("", fit_buttons_layout) + fitInputLayout = qt.QFormLayout() + fitInputLayout.addRow("Data: ", self.__fitDataInputComboBox) + fitInputLayout.addRow("Equation: ", self.__fitEquationComboBox) + fitInputLayout.addRow("", fitButtonsLayout) - fit_input_frame = qt.QFrame() - fit_input_frame.setLayout(fit_input_layout) + fitInputFrame = qt.QFrame() + fitInputFrame.setLayout(fitInputLayout) ## Input layout - input_layout = qt.QVBoxLayout() - input_layout.addWidget(fit_input_frame) + inputLayout = qt.QVBoxLayout() + inputLayout.addWidget(fitInputFrame) - self.input_collapsible = ctk.ctkCollapsibleButton() - self.input_collapsible.text = "Input" - self.input_collapsible.setLayout(input_layout) + self.inputCollapsible = ctk.ctkCollapsibleButton() + self.inputCollapsible.text = "Input" + self.inputCollapsible.setLayout(inputLayout) # Parameters ## Parameters stack - self.fitted_curves_combobox = qt.QComboBox() - self.fitted_curves_combobox.addItem("") - self.fitted_curves_combobox.currentTextChanged.connect(self.__on_fitted_curve_selected) + self.fittedCurvesComboBox = qt.QComboBox() + self.fittedCurvesComboBox.addItem("") + self.fittedCurvesComboBox.currentTextChanged.connect(self.__onFittedCurveSelected) - self.parameters_stack = qt.QStackedWidget() - for fit_equation in self.__fit_equations: - equation_widget = fit_equation.widget.get_widget() - self.parameters_stack.addWidget(equation_widget) - equation_widget.signal_parameter_changed.connect(self.__on_equation_changed) - equation_widget.refit_button_pressed.connect(self.__on_refit_button_clicked) + self.parametersStack = qt.QStackedWidget() + for fitEquation in self.__fitEquations: + equationWidget = fitEquation.widget.get_widget() + self.parametersStack.addWidget(equationWidget) + equationWidget.signal_parameter_changed.connect(self.__onEquationChanged) + equationWidget.refit_button_pressed.connect(self.__on_refit_button_clicked) - parameters_layout = qt.QFormLayout() - parameters_layout.addRow("Fitted curve: ", self.fitted_curves_combobox) - parameters_layout.addRow(self.parameters_stack) + parametersLayout = qt.QFormLayout() + parametersLayout.addRow("Fitted curve: ", self.fittedCurvesComboBox) + parametersLayout.addRow(self.parametersStack) - self.parameters_collapsible = ctk.ctkCollapsibleButton() - self.parameters_collapsible.text = "Parameters" - self.parameters_collapsible.setLayout(parameters_layout) + self.parametersCollapsible = ctk.ctkCollapsibleButton() + self.parametersCollapsible.text = "Parameters" + self.parametersCollapsible.setLayout(parametersLayout) - self.__select_fit_equation(self.__fit_equations[0].widget.DISPLAY_NAME) + self.__selectFitEquation(self.__fitEquations[0].widget.DISPLAY_NAME) # Output - self.__save_button = qt.QPushButton("Save to project") - self.__save_button.setFocusPolicy(qt.Qt.NoFocus) - self.__save_button.clicked.connect(self.__on_fit_save_button_clicked) + self.__saveButton = qt.QPushButton("Save to project") + self.__saveButton.setFocusPolicy(qt.Qt.NoFocus) + self.__saveButton.clicked.connect(self.__onFitSaveButtonClicked) - output_layout = qt.QFormLayout() - output_layout.addRow("", self.__save_button) + outputLayout = qt.QFormLayout() + outputLayout.addRow("", self.__saveButton) - output_collapsible = ctk.ctkCollapsibleButton() - output_collapsible.text = "Output: " - output_collapsible.setLayout(output_layout) + outputCollapsible = ctk.ctkCollapsibleButton() + outputCollapsible.text = "Output: " + outputCollapsible.setLayout(outputLayout) # Layout - fit_tab_layout = qt.QVBoxLayout() - fit_tab_layout.addWidget(self.input_collapsible) - fit_tab_layout.addWidget(self.parameters_collapsible) - fit_tab_layout.addWidget(output_collapsible) - fit_tab_layout.addStretch() - - self.__fit_tab_widget = qt.QFrame() - self.__fit_tab_widget.setLayout(fit_tab_layout) - return self.__fit_tab_widget - - def __select_fit_equation(self, selected_equation): - self.fitted_curves_combobox.setCurrentText("") - - for index, fit_equation in enumerate(self.__fit_equations): - if fit_equation.widget.DISPLAY_NAME == selected_equation: - self.__set_current_parameters_stack(index) + fitTabLayout = qt.QVBoxLayout() + fitTabLayout.addWidget(self.inputCollapsible) + fitTabLayout.addWidget(self.parametersCollapsible) + fitTabLayout.addWidget(outputCollapsible) + fitTabLayout.addStretch() + + self.__fitTabWidget = qt.QFrame() + self.__fitTabWidget.setLayout(fitTabLayout) + return self.__fitTabWidget + + def __selectFitEquation(self, selected_equation): + self.fittedCurvesComboBox.setCurrentText("") + + for index, fitEquation in enumerate(self.__fitEquations): + if fitEquation.widget.DISPLAY_NAME == selected_equation: + self.__setCurrentParametersStack(index) break - def __set_current_parameters_stack(self, index): - self.parameters_stack.setCurrentIndex(index) - self.parameters_collapsible.setMaximumHeight(125 + 50 * len(self.__fit_equations[index].widget.PARAMETERS)) - - def __on_fitted_curve_selected(self, fitted_curve): - self.__valid_fitted_curve_selected = False - if not fitted_curve: - self.__set_current_fitted_curve(None) - for fit_data in self.__fit_data_list: - if fit_data.name == fitted_curve: - self.__set_current_fitted_curve(fit_data) - self.__valid_fitted_curve_selected = True + def __setCurrentParametersStack(self, index): + self.parametersStack.setCurrentIndex(index) + self.parametersCollapsible.setMaximumHeight(125 + 50 * len(self.__fitEquations[index].widget.PARAMETERS)) + + def __onFittedCurveSelected(self, fittedCurve): + self.__validFittedCurveSelected = False + if not fittedCurve: + self.__setCurrentFittedCurve(None) + for fitData in self.__fitDataList: + if fitData.name == fittedCurve: + self.__setCurrentFittedCurve(fitData) + self.__validFittedCurveSelected = True break - self.__update_refit_button_state() - self.__update_output_button_state() + self.__updateRefitButtonState() + self.__updateOutputButtonState() - def __on_fit_clicked(self): - input_data = self.__get_current_input_data() - if input_data is None: + def __onFitClicked(self): + inputData = self.__getCurrentInputData() + if inputData is None: return - for fit_equation in self.__fit_equations: - if self.__fit_equation_combo_box.currentText == fit_equation.widget.DISPLAY_NAME: - fit_data = fit_equation.equation.fit("temp_curve", input_data["x"], input_data["y"]) + for fitEquation in self.__fitEquations: + if self.__fitEquationComboBox.currentText == fitEquation.widget.DISPLAY_NAME: + fitData = fitEquation.equation.fit( + self.__fitDataInputComboBox.currentText, inputData["x"], inputData["y"] + ) break - fit_data.style.color = input_data["color"] - self.__add_fit_data(fit_data) - - def __on_import_clicked(self): - file_dialog = qt.QFileDialog(self.__fit_tab_widget, "Select function") - file_dialog.setNameFilters(["Table file (*.tsv)"]) - if file_dialog.exec(): - paths = file_dialog.selectedFiles() - imported_volume = slicer.util.loadTable(paths[0]) - if imported_volume.GetColumnName(0) != "Fitting equation": - slicer.mrmlScene.RemoveNode(imported_volume) - slicer.util.errorDisplay("Couldn't import the file as a fitted function", parent=self.__fit_tab_widget) + fitData.style.color = inputData["color"] + self.__addFitData(fitData) + + def __onImportClicked(self): + fileDialog = qt.QFileDialog(self.__fitTabWidget, "Select function") + fileDialog.setNameFilters(["Table file (*.tsv)"]) + if fileDialog.exec(): + paths = fileDialog.selectedFiles() + importedVolume = slicer.util.loadTable(paths[0]) + if importedVolume.GetColumnName(0) != "Fitting equation": + slicer.mrmlScene.RemoveNode(importedVolume) + slicer.util.errorDisplay("Couldn't import the file as a fitted function", parent=self.__fitTabWidget) else: - imported_volume.SetAttribute("table_type", "equation") - self.appendData(imported_volume) + importedVolume.SetAttribute("table_type", "equation") + self.appendData(importedVolume) - def __on_export_clicked(self, function_curve_name): - fit_data = self.__get_fit_data(function_curve_name) + def __onExportClicked(self, functionCurveName): + fitData = self.__getFitData(functionCurveName) path = qt.QFileDialog.getSaveFileName( - None, "Save file", f"{function_curve_name}.tsv", "Tab-separated values (*.tsv)" + None, "Save file", f"{functionCurveName}.tsv", "Tab-separated values (*.tsv)" ) if path: - for fit_equation in self.__fit_equations: - if fit_data.type == fit_equation.equation.NAME: - df = fit_equation.equation.to_df(fit_data) + for fitEquation in self.__fitEquations: + if fitData.type == fitEquation.equation.NAME: + df = fitEquation.equation.to_df(fitData) df.to_csv(path, sep="\t", index=False) break - def __on_equation_changed(self, parameter_name: str, new_value: float, is_fixed: bool): - fit_data = self.__get_current_fit_data() - if not fit_data: + def __onEquationChanged(self, parameterName: str, newValue: float, isFixed: bool): + fitData = self.__getCurrentFitData() + if not fitData: return - if is_fixed: - fixed_parameters = fit_data.fixed_parameters - if parameter_name not in fixed_parameters: - fixed_parameters.append(parameter_name) - fit_data.fixed_parameters = fixed_parameters - self.__update_function_curve(fit_data, parameter_name, new_value) - self.__set_current_function_curve_data(fit_data) - - def __on_function_curve_edited(self, function_curve: str, parameter_name: str, new_value: float): - fit_data = self.__get_fit_data(function_curve) - if not fit_data: + if isFixed: + fixed_parameters = fitData.fixed_parameters + if parameterName not in fixed_parameters: + fixed_parameters.append(parameterName) + fitData.fixed_parameters = fixed_parameters + self.__updateFunctionCurve(fitData, parameterName, newValue) + self.__setCurrentFunctionCurveData(fitData) + + def __onFunctionCurveEdited(self, function_curve: str, parameterName: str, newValue: float): + fitData = self.__getFitData(function_curve) + if not fitData: return - self.__update_function_curve(fit_data, parameter_name, new_value) - self.__set_current_function_curve_data(fit_data) - - def __update_function_curve(self, fit_data: FitData, parameter_name: str, new_value: float): - fit_data.set_parameter(parameter_name, new_value) - for fit_equation in self.__fit_equations: - if fit_data.type == fit_equation.equation.NAME: - fit_data.y = fit_equation.equation.equation(fit_data.x, fit_data.parameters) + self.__updateFunctionCurve(fitData, parameterName, newValue) + self.__setCurrentFunctionCurveData(fitData) + + def __updateFunctionCurve(self, fitData: FitData, parameterName: str, newValue: float): + fitData.set_parameter(parameterName, newValue) + for fitEquation in self.__fitEquations: + if fitData.type == fitEquation.equation.NAME: + fitData.y = fitEquation.equation.equation(fitData.x, fitData.parameters) break self.__updatePlot() - def __set_current_fitted_curve(self, fit_data): - if fit_data is None: - for fit_equation in self.__fit_equations: - fit_equation.widget.clear() + def __setCurrentFittedCurve(self, fitData): + if fitData is None: + for fitEquation in self.__fitEquations: + fitEquation.widget.clear() return - for index, fit_equation in enumerate(self.__fit_equations): - if fit_equation.equation.NAME == fit_data.type: - fit_equation.widget.update(fit_data) - self.__set_current_parameters_stack(index) + for index, fitEquation in enumerate(self.__fitEquations): + if fitEquation.equation.NAME == fitData.type: + fitEquation.widget.update(fitData) + self.__setCurrentParametersStack(index) return - def __set_current_function_curve_data(self, fit_data: FitData): - self.__set_current_fitted_curve(fit_data) - self.equations_tab.set_current_function_curve_data(fit_data) + def __setCurrentFunctionCurveData(self, fitData: FitData): + self.__setCurrentFittedCurve(fitData) + self.equationsTab.setCurrentFunctionCurveData(fitData) def __on_refit_button_clicked(self): - fit_data = self.__get_current_fit_data() - input_data = self.__get_current_input_data() - for fit_equation in self.__fit_equations: - if fit_equation.equation.NAME == fit_data.type: - fixed_values = fit_equation.widget.get_fixed_values() - custom_bounds = fit_equation.widget.get_custom_bounds() - if None not in fixed_values: + fitData = self.__getCurrentFitData() + inputData = self.__getCurrentInputData() + for fitEquation in self.__fitEquations: + if fitEquation.equation.NAME == fitData.type: + fixedValues = fitEquation.widget.get_fixed_values() + customBounds = fitEquation.widget.get_custom_bounds() + if None not in fixedValues: slicer.util.errorDisplay( - "All values are fixed. There's no refit to be made.", parent=self.__fit_tab_widget + "All values are fixed. There's no refit to be made.", parent=self.__fitTabWidget ) return - new_fit_data = fit_equation.equation.fit( - fit_data.name, input_data["x"], input_data["y"], fixed_values, custom_bounds + newFitData = fitEquation.equation.fit( + fitData.name, inputData["x"], inputData["y"], fixedValues, customBounds ) - new_fit_data.style.color = input_data["color"] - new_fit_data.style.size = 1 - self.__add_fit_data(new_fit_data) + newFitData.style.color = inputData["color"] + newFitData.style.size = 1 + self.__addFitData(newFitData) def __update_fitted_curves_plot(self): - for fit_data in self.__fit_data_list: - if fit_data.visible: - self.data_plot_widget.add_curve_plot(fit_data) + for fitData in self.__fitDataList: + if fitData.visible: + self.dataPlotWidget.add_curve_plot(fitData) def appendData(self, dataNode: slicer.vtkMRMLNode): if dataNode.GetAttribute("table_type") == "equation": - self.__add_equation_data_from_table(dataNode) + self.__addEquationDataFromTable(dataNode) else: self.appendDataNode(dataNode) @@ -700,23 +721,23 @@ def __updateAxisComboBoxes(self): self.__zAxisComboBox.setCurrentText(currentZAxis) def __updateCurveFittingComboBoxes(self): - current_data_input = self.__fit_data_input_combo_box.currentText - self.__fit_data_input_combo_box.clear() - self.fitted_curves_combobox.clear() + current_data_input = self.__fitDataInputComboBox.currentText + self.__fitDataInputComboBox.clear() + self.fittedCurvesComboBox.clear() - self.fitted_curves_combobox.addItem("") + self.fittedCurvesComboBox.addItem("") for graph_data in self.__graphDataList: - self.__fit_data_input_combo_box.addItem(graph_data.name) + self.__fitDataInputComboBox.addItem(graph_data.name) - function_curve_names = [] - for fit_data in self.__fit_data_list: - function_curve_names.append(fit_data.name) - self.fitted_curves_combobox.addItems(function_curve_names) - self.equations_tab.set_functions_curves(function_curve_names) + functionCurveNames = [] + for fitData in self.__fitDataList: + functionCurveNames.append(fitData.name) + self.fittedCurvesComboBox.addItems(functionCurveNames) + self.equationsTab.setFunctionsCurves(functionCurveNames) if current_data_input: - self.__fit_data_input_combo_box.setCurrentText(current_data_input) + self.__fitDataInputComboBox.setCurrentText(current_data_input) def __createPlotUpdateTimer(self): """Initialize timer object that process data to plot""" @@ -738,7 +759,7 @@ def __updatePlot(self): def __handleUpdatePlot(self): """Wrapper for updating the plots related to the user's input (2D or 3D plot)""" - self.data_plot_widget.clear_plot() + self.dataPlotWidget.clear_plot() if ( self.__xAxisComboBox.currentText() != AXIS_NONE_PARAMETER @@ -762,7 +783,7 @@ def __handleUpdatePlot(self): self.__updatePlotsLayout() self.__update_fitted_curves_plot() - self.data_plot_widget.auto_range() + self.dataPlotWidget.auto_range() def __update2dPlot(self, xAxisParameter, yAxisParameter, graph_data_list): """Handles 2D plot updates""" @@ -789,7 +810,7 @@ def __update2dPlot(self, xAxisParameter, yAxisParameter, graph_data_list): yData = self.__yUnitConversion.convert(yData) yAxisName = yAxisName.split()[0] + f" ({self.__yUnitConversion.currentUnits()[1]})" - self.data_plot_widget.add2dPlot(graphData, xData, yData, xAxisName, yAxisName) + self.dataPlotWidget.add2dPlot(graphData, xData, yData, xAxisName, yAxisName) def __update3dPlot(self, xAxisParameter, yAxisParameter, zAxisParameter, graph_data_list): """Handles 3D plot updates""" @@ -838,6 +859,7 @@ def __update3dPlot(self, xAxisParameter, yAxisParameter, zAxisParameter, graph_d self.__ZMaxValueRangeDoubleSpinBox.setValue(zMax or 0) # Plot + zAxisName = "" for graphData in enabled_graph_data_list: xData = graphData.data.get(xAxisParameter, None) yData = graphData.data.get(yAxisParameter, None) @@ -857,9 +879,9 @@ def __update3dPlot(self, xAxisParameter, yAxisParameter, zAxisParameter, graph_d zData = self.__zUnitConversion.convert(zData) zAxisName = zAxisName.split()[0] + f" ({self.__zUnitConversion.currentUnits()[1]})" - self.data_plot_widget.add3dPlot(graphData, xData, yData, zData, xAxisName, yAxisName, zAxisName, zMin, zMax) + self.dataPlotWidget.add3dPlot(graphData, xData, yData, zData, xAxisName, yAxisName, zAxisName, zMin, zMax) - self.data_plot_widget.update_legend_item(zAxisName) + self.dataPlotWidget.update_legend_item(zAxisName) def __updateGraphDataTable(self): """Handles table widget data update""" @@ -868,8 +890,8 @@ def __updateGraphDataTable(self): for graphData in self.__graphDataList: self.__tableWidget.add_data(graphData, DataTableWidget.INPUT_DATA_TYPE) - for fit_data in self.__fit_data_list: - self.__tableWidget.add_data(fit_data, DataTableWidget.FIT_DATA_TYPE) + for fitData in self.__fitDataList: + self.__tableWidget.add_data(fitData, DataTableWidget.FIT_DATA_TYPE) def __removeGraphDataFromTable(self, graphData: NodeGraphData): """Remove data and objects related to the GraphData object.""" @@ -882,12 +904,12 @@ def __removeGraphDataFromTable(self, graphData: NodeGraphData): self.__updateCurveFittingComboBoxes() self.__updatePlot() - def __removeFitDataFromTable(self, fit_data: FitData): + def __removeFitDataFromTable(self, fitData: FitData): """Remove data and objects related to the FitData object.""" - if not fit_data in self.__fit_data_list: + if not fitData in self.__fitDataList: return - self.__fit_data_list.remove(fit_data) + self.__fitDataList.remove(fitData) self.__updateGraphDataTable() self.__updatePlot() @@ -897,9 +919,9 @@ def __removeDataFromTableByName(self, name): self.__removeGraphDataFromTable(graph_data) return - for fit_data in self.__fit_data_list: - if fit_data.name == name: - self.__removeFitDataFromTable(fit_data) + for fitData in self.__fitDataList: + if fitData.name == name: + self.__removeFitDataFromTable(fitData) return def __populateColorMapComboBox(self): @@ -908,7 +930,7 @@ def __populateColorMapComboBox(self): self.__colorMapComboBox.addItem(QtGui.QIcon(colorMapInfo.reference_image), colorMapInfo.label) self.__colorMapComboBox.setCurrentText("Gist Rainbow") - self.data_plot_widget.set_colormap(matplotlibcm.gist_rainbow.name) + self.dataPlotWidget.set_colormap(matplotlibcm.gist_rainbow.name) def __onColorMapComboBoxChanged(self, text): """Handles color map combo box options change event.""" @@ -916,7 +938,7 @@ def __onColorMapComboBoxChanged(self, text): if text != colorMapInfo.label: continue - self.data_plot_widget.set_colormap(colorMapInfo.object.name) + self.dataPlotWidget.set_colormap(colorMapInfo.object.name) self.__updatePlot() break @@ -960,8 +982,8 @@ def __hasValidZAxisRange(self): ) def __plotHistograms(self): - self.data_plot_widget.clear_histogram_x() - self.data_plot_widget.clear_histogram_y() + self.dataPlotWidget.clear_histogram_x() + self.dataPlotWidget.clear_histogram_y() if ( self.__xAxisHistogramEnableCheckBox.isChecked() is False @@ -997,7 +1019,7 @@ def createHistogramPlots(graphData, axisParameter, bins): graphData, xAxisParameter, self.__xHistogramBinSpinBox.value() ) if xHistogram is not None and yHistogram is not None: - self.data_plot_widget.add_histogram_plot_x(graphData, xHistogram, yHistogram) + self.dataPlotWidget.add_histogram_plot_x(graphData, xHistogram, yHistogram) if self.__yAxisHistogramEnableCheckBox.isChecked() is True: for graphData in self.__graphDataList: @@ -1005,14 +1027,14 @@ def createHistogramPlots(graphData, axisParameter, bins): graphData, yAxisParameter, self.__yHistogramBinSpinBox.value() ) if xHistogram is not None and yHistogram is not None: - self.data_plot_widget.add_histogram_plot_y(graphData, xHistogram, yHistogram) + self.dataPlotWidget.add_histogram_plot_y(graphData, xHistogram, yHistogram) def __onHistogramCheckBoxChange(self, state): if self.__xAxisHistogramEnableCheckBox.isChecked() is False: - self.data_plot_widget.clear_histogram_x() + self.dataPlotWidget.clear_histogram_x() if self.__yAxisHistogramEnableCheckBox.isChecked() is False: - self.data_plot_widget.clear_histogram_y() + self.dataPlotWidget.clear_histogram_y() self.__updatePlot() @@ -1023,124 +1045,124 @@ def __onHistogramBinChange(self, value): self.__updatePlot() def __updateLogMode(self): - self.data_plot_widget.set_log_mode(x=self.__xLogCheckBox.isChecked(), y=self.__yLogCheckBox.isChecked()) + self.dataPlotWidget.set_log_mode(x=self.__xLogCheckBox.isChecked(), y=self.__yLogCheckBox.isChecked()) def __updatePlotsLayout(self): - self.data_plot_widget.set_theme(self.__themeComboBox.currentText()) - self.data_plot_widget._updatePlotsLayout( + self.dataPlotWidget.set_theme(self.__themeComboBox.currentText()) + self.dataPlotWidget._updatePlotsLayout( self.__xAxisHistogramEnableCheckBox.isChecked(), self.__yAxisHistogramEnableCheckBox.isChecked(), self.__zAxisComboBox.currentText(), ) - def __update_refit_button_state(self): - for fit_equation in self.__fit_equations: - fit_equation.widget.update_refit_button_state(self.__valid_fitted_curve_selected) + def __updateRefitButtonState(self): + for fitEquation in self.__fitEquations: + fitEquation.widget.update_refit_button_state(self.__validFittedCurveSelected) - def __update_output_button_state(self): - self.__save_button.enabled = self.__valid_fitted_curve_selected + def __updateOutputButtonState(self): + self.__saveButton.enabled = self.__validFittedCurveSelected - def __on_fit_save_button_clicked(self): - fit_data = self.__get_current_fit_data() - if fit_data is None: + def __onFitSaveButtonClicked(self): + fitData = self.__getCurrentFitData() + if fitData is None: return - self.__save_data_to_node(fit_data, fit_data.name) + self.__saveDataToNode(fitData, fitData.name) - def __on_function_curve_save_button_clicked(self, function_curve_name): - fit_data = self.__get_fit_data(function_curve_name) - self.__save_data_to_node(fit_data, function_curve_name) + def __on_function_curve_save_button_clicked(self, functionCurveName): + fitData = self.__getFitData(functionCurveName) + self.__saveDataToNode(fitData, functionCurveName) - def __save_data_to_node(self, fit_data, new_name): - table_node = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLTableNode.__name__) - table_node.SetName(slicer.mrmlScene.GenerateUniqueName(new_name)) + def __saveDataToNode(self, fitData, new_name): + tableNode = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLTableNode.__name__) + tableNode.SetName(slicer.mrmlScene.GenerateUniqueName(new_name)) - table_modification = table_node.StartModify() + table_modification = tableNode.StartModify() - column_fitting_equation = vtk.vtkStringArray() - column_fitting_equation.SetName("Fitting equation") - column_fitting_equation.InsertNextValue(fit_data.type) - table_node.AddColumn(column_fitting_equation) + columnFittingEquation = vtk.vtkStringArray() + columnFittingEquation.SetName("Fitting equation") + columnFittingEquation.InsertNextValue(fitData.type) + tableNode.AddColumn(columnFittingEquation) - for fit_equation in self.__fit_equations: - if fit_equation.equation.NAME == fit_data.type: - fit_equation.equation.append_to_node(fit_data, table_node) + for fitEquation in self.__fitEquations: + if fitEquation.equation.NAME == fitData.type: + fitEquation.equation.append_to_node(fitData, tableNode) break - table_node.SetAttribute("table_type", "equation") - table_node.Modified() - table_node.EndModify(table_modification) - - OUTPUT_DIR_NAME = "Math functions" - subject_hierarchy = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - app_folder_id = subject_hierarchy.GetSceneItemID() - output_dir = subject_hierarchy.GetItemChildWithName(app_folder_id, OUTPUT_DIR_NAME) - if output_dir == 0: - output_dir = subject_hierarchy.CreateFolderItem( - app_folder_id, helpers.generateName(subject_hierarchy, OUTPUT_DIR_NAME) + tableNode.SetAttribute("table_type", "equation") + tableNode.Modified() + tableNode.EndModify(table_modification) + + outputDirName = "Math functions" + subjectHierarchy = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + appFolderId = subjectHierarchy.GetSceneItemID() + outputDir = subjectHierarchy.GetItemChildWithName(appFolderId, outputDirName) + if outputDir == 0: + outputDir = subjectHierarchy.CreateFolderItem( + appFolderId, helpers.generateName(subjectHierarchy, outputDirName) ) - subject_hierarchy.CreateItem(output_dir, table_node) + subjectHierarchy.CreateItem(outputDir, tableNode) - fit_data.name = new_name + fitData.name = new_name self.__updateCurveFittingComboBoxes() self.__updateGraphDataTable() - def __add_equation_data_from_table(self, table_node): - vtk_table = table_node.GetTable() - equation_type = vtk_table.GetValueByName(0, "Fitting equation") - for fit_equation in self.__fit_equations: - if fit_equation.equation.NAME == equation_type: - fit_data = fit_equation.equation.from_table(table_node.GetName(), vtk_table) + def __addEquationDataFromTable(self, tableNode): + vtkTable = tableNode.GetTable() + equation_type = vtkTable.GetValueByName(0, "Fitting equation") + for fitEquation in self.__fitEquations: + if fitEquation.equation.NAME == equation_type: + fitData = fitEquation.equation.from_table(tableNode.GetName(), vtkTable) break - self.__add_fit_data(fit_data) + self.__addFitData(fitData) - def __on_fit_data_created(self, fit_data): - self.__add_fit_data(fit_data) - self.__set_current_function_curve_data(fit_data) + def __onFitDataCreated(self, fitData): + self.__addFitData(fitData) + self.__setCurrentFunctionCurveData(fitData) - def __add_fit_data(self, new_fit_data: FitData): - for fit_data in self.__fit_data_list: - if fit_data.name == new_fit_data.name: - self.__fit_data_list.remove(fit_data) + def __addFitData(self, newFitData: FitData): + for fitData in self.__fitDataList: + if fitData.name == newFitData.name: + self.__fitDataList.remove(fitData) break - new_fit_data.style.size = 1 - self.__fit_data_list.append(new_fit_data) - new_fit_data.signalVisibleChanged.connect(self.__updatePlot) + newFitData.style.size = 1 + self.__fitDataList.append(newFitData) + newFitData.signalVisibleChanged.connect(self.__updatePlot) self.__updatePlot() self.__updateGraphDataTable() self.__updateCurveFittingComboBoxes() - self.fitted_curves_combobox.setCurrentText(new_fit_data.name) + self.fittedCurvesComboBox.setCurrentText(newFitData.name) - def __get_current_fit_data(self): - return self.__get_fit_data(self.fitted_curves_combobox.currentText) + def __getCurrentFitData(self): + return self.__getFitData(self.fittedCurvesComboBox.currentText) - def __get_fit_data(self, name): - for fit_data in self.__fit_data_list: - if fit_data.name == name: - return fit_data + def __getFitData(self, name): + for fitData in self.__fitDataList: + if fitData.name == name: + return fitData return None - def __get_current_input_data(self): + def __getCurrentInputData(self): try: - input_data = self.data_plot_widget.get_plotted_data(self.__fit_data_input_combo_box.currentText) + inputData = self.dataPlotWidget.get_plotted_data(self.__fitDataInputComboBox.currentText) except KeyError: slicer.util.errorDisplay( "Nothing related to this data is plotted. " "Please assign parameters from this input data in the Data tab or select another one.", - parent=self.__fit_tab_widget, + parent=self.__fitTabWidget, ) return None - if input_data["x"] is None or input_data["y"] is None: + if inputData["x"] is None or inputData["y"] is None: slicer.util.errorDisplay( "Missing 2D data. You must select data for X and Y axes before fitting to the equation.", - parent=self.__fit_tab_widget, + parent=self.__fitTabWidget, ) return None - return input_data + return inputData def __updateAllDataStyles(self, symbol, symbol_size, line_style, line_size): for graphData in self.__graphDataList: @@ -1162,98 +1184,107 @@ def __updateAllDataVisibility(self, visibility_state): self.__updateGraphDataTable() + def __onEmbeddedLegendVisibilityChange(self, state): + if not self.dataPlotWidget: + return + + self.dataPlotWidget.embeddedLegendVisibility = state == qt.Qt.Checked + + def __toggleLegend(self): + self.__embeddedLegendVisibilityCheckBox.setChecked(self.dataPlotWidget.embeddedLegendVisibility) + class EquationParametersWidget(qt.QFrame): - signal_function_curve_selected = qt.Signal(str) - signal_function_curve_edited = qt.Signal(str, str, float, bool) + signalFunctionCurveSelected = qt.Signal(str) + signalFunctionCurveEdited = qt.Signal(str, str, float, bool) def __init__(self): super().__init__() - self.__fit_equations = [Line(False), TimurCoates(False)] + self.__fitEquations = [Line(False), TimurCoates(False)] - self.function_curves_combobox = qt.QComboBox() - self.function_curves_combobox.addItem("") - self.function_curves_combobox.currentTextChanged.connect(self.__on_function_curve_selected) + self.functionCurvesComboBox = qt.QComboBox() + self.functionCurvesComboBox.addItem("") + self.functionCurvesComboBox.currentTextChanged.connect(self.__onFunctionCurveSelected) - self.parameters_stack = ShrinkableStackedWidget() - for fit_equation in self.__fit_equations: - equation_widget = fit_equation.widget.get_widget() - self.parameters_stack.addWidget(equation_widget) - equation_widget.signal_parameter_changed.connect(self.__on_equation_changed) - self.parameters_stack.setCurrentIndex(0) + self.parametersStack = ShrinkableStackedWidget() + for fitEquation in self.__fitEquations: + equationWidget = fitEquation.widget.get_widget() + self.parametersStack.addWidget(equationWidget) + equationWidget.signal_parameter_changed.connect(self.__onEquationChanged) + self.parametersStack.setCurrentIndex(0) - edit_layout = qt.QFormLayout() - edit_layout.setContentsMargins(0, 0, 0, 0) - edit_layout.addRow("Function:", self.function_curves_combobox) - edit_layout.addRow(self.parameters_stack) + editLayout = qt.QFormLayout() + editLayout.setContentsMargins(0, 0, 0, 0) + editLayout.addRow("Function:", self.functionCurvesComboBox) + editLayout.addRow(self.parametersStack) - self.parameters_collapsible = ctk.ctkCollapsibleButton(self) - self.parameters_collapsible.text = "Parameters" - self.parameters_collapsible.setLayout(edit_layout) + self.parametersCollapsible = ctk.ctkCollapsibleButton(self) + self.parametersCollapsible.text = "Parameters" + self.parametersCollapsible.setLayout(editLayout) layout = qt.QVBoxLayout() - layout.addWidget(self.parameters_collapsible) + layout.addWidget(self.parametersCollapsible) self.setLayout(layout) - def set_functions_curves(self, function_curve_list: list): - self.function_curves_combobox.clear() - self.function_curves_combobox.addItem("") - self.function_curves_combobox.addItems(function_curve_list) + def setFunctionsCurves(self, function_curve_list: list): + self.functionCurvesComboBox.clear() + self.functionCurvesComboBox.addItem("") + self.functionCurvesComboBox.addItems(function_curve_list) - def set_current_function_curve_data(self, curve_data): + def setCurrentFunctionCurveData(self, curve_data): if curve_data is None: - for fit_equation in self.__fit_equations: - fit_equation.widget.clear() + for fitEquation in self.__fitEquations: + fitEquation.widget.clear() return - for index, fit_equation in enumerate(self.__fit_equations): - if fit_equation.equation.NAME == curve_data.type: - fit_equation.widget.update(curve_data) - self.__set_current_parameters_stack(index) - self.function_curves_combobox.setCurrentText(curve_data.name) + for index, fitEquation in enumerate(self.__fitEquations): + if fitEquation.equation.NAME == curve_data.type: + fitEquation.widget.update(curve_data) + self.__setCurrentParametersStack(index) + self.functionCurvesComboBox.setCurrentText(curve_data.name) break - def get_current_function_curve_name(self): - return self.function_curves_combobox.currentText + def getCurrentFunctionCurveName(self): + return self.functionCurvesComboBox.currentText - def __on_function_curve_selected(self, function_curve): - self.signal_function_curve_selected.emit(function_curve) + def __onFunctionCurveSelected(self, function_curve): + self.signalFunctionCurveSelected.emit(function_curve) - def __set_current_parameters_stack(self, index): - self.parameters_stack.setCurrentIndex(index) + def __setCurrentParametersStack(self, index): + self.parametersStack.setCurrentIndex(index) - def __on_equation_changed(self, parameter_name, new_value, is_fixed): - function_curve = self.function_curves_combobox.currentText - self.signal_function_curve_edited.emit(function_curve, parameter_name, new_value, is_fixed) + def __onEquationChanged(self, parameterName, newValue, isFixed): + function_curve = self.functionCurvesComboBox.currentText + self.signalFunctionCurveEdited.emit(function_curve, parameterName, newValue, isFixed) class EquationsTabWidget(qt.QFrame): - signal_new_function_curve_data = qt.Signal(FitData) - signal_import_function_curve = qt.Signal() - signal_export_function_curve = qt.Signal(str) - signal_function_curve_edited = qt.Signal(str, str, float) - signal_save_data = qt.Signal(str) + signalNewFunctionCurveData = qt.Signal(FitData) + signalImportFunctionCurve = qt.Signal() + signalExportFunctionCurve = qt.Signal(str) + signalFunctionCurveEdited = qt.Signal(str, str, float) + signalSaveData = qt.Signal(str) def __init__(self, fit_data_list): super().__init__() - self.__fit_data_list = fit_data_list + self.__fitDataList = fit_data_list # Input layout self.__functionCurveNameLineEdit = qt.QLineEdit() - self.__fit_equation_combo_box = qt.QComboBox() - self.__fit_equations = [Line(False), TimurCoates(False)] - for fit_equation in self.__fit_equations: - self.__fit_equation_combo_box.addItem(fit_equation.widget.DISPLAY_NAME) + self.__fitEquationComboBox = qt.QComboBox() + self.__fitEquations = [Line(False), TimurCoates(False)] + for fitEquation in self.__fitEquations: + self.__fitEquationComboBox.addItem(fitEquation.widget.DISPLAY_NAME) createButton = qt.QPushButton("Create") createButton.setFocusPolicy(qt.Qt.NoFocus) - createButton.clicked.connect(self.__on_create_clicked) + createButton.clicked.connect(self.__onCreateClicked) importButton = qt.QPushButton("Import") importButton.setFocusPolicy(qt.Qt.NoFocus) - importButton.clicked.connect(self.__on_import_clicked) + importButton.clicked.connect(self.__onImportClicked) inputButtonLayout = qt.QHBoxLayout() inputButtonLayout.addWidget(createButton) @@ -1261,7 +1292,7 @@ def __init__(self, fit_data_list): inputLayout = qt.QFormLayout() inputLayout.addRow("Name:", self.__functionCurveNameLineEdit) - inputLayout.addRow("Equation:", self.__fit_equation_combo_box) + inputLayout.addRow("Equation:", self.__fitEquationComboBox) inputLayout.addRow("", inputButtonLayout) inputCollapsible = ctk.ctkCollapsibleButton() @@ -1269,86 +1300,86 @@ def __init__(self, fit_data_list): inputCollapsible.setLayout(inputLayout) # Edit layout - self.edit_collapsible = EquationParametersWidget() - self.edit_collapsible.signal_function_curve_selected.connect(self.__on_function_curve_selected) - self.edit_collapsible.signal_function_curve_edited.connect(self.__on_function_curve_edited) + self.editCollapsible = EquationParametersWidget() + self.editCollapsible.signalFunctionCurveSelected.connect(self.__onFunctionCurveSelected) + self.editCollapsible.signalFunctionCurveEdited.connect(self.__onFunctionCurveEdited) # Output layout - self.__save_button = qt.QPushButton("Save to project") - self.__save_button.setFocusPolicy(qt.Qt.NoFocus) - self.__save_button.clicked.connect(self.__on_save_button_clicked) + self.__saveButton = qt.QPushButton("Save to project") + self.__saveButton.setFocusPolicy(qt.Qt.NoFocus) + self.__saveButton.clicked.connect(self.__on_save_button_clicked) self.__exportButton = qt.QPushButton("Export to file") self.__exportButton.setFocusPolicy(qt.Qt.NoFocus) - self.__exportButton.clicked.connect(self.__on_export_clicked) + self.__exportButton.clicked.connect(self.__onExportClicked) - output_layout = qt.QHBoxLayout() - output_layout.addWidget(self.__save_button) - output_layout.addWidget(self.__exportButton) + outputLayout = qt.QHBoxLayout() + outputLayout.addWidget(self.__saveButton) + outputLayout.addWidget(self.__exportButton) - output_collapsible = ctk.ctkCollapsibleButton() - output_collapsible.text = "Output" - output_collapsible.setLayout(output_layout) + outputCollapsible = ctk.ctkCollapsibleButton() + outputCollapsible.text = "Output" + outputCollapsible.setLayout(outputLayout) # Equations tab equations_tab_layout = qt.QVBoxLayout() equations_tab_layout.addWidget(inputCollapsible) - equations_tab_layout.addWidget(self.edit_collapsible) - equations_tab_layout.addWidget(output_collapsible) + equations_tab_layout.addWidget(self.editCollapsible) + equations_tab_layout.addWidget(outputCollapsible) equations_tab_layout.addStretch() self.setLayout(equations_tab_layout) - self.__on_function_curve_selected("") + self.__onFunctionCurveSelected("") - def set_functions_curves(self, function_curve_list: list): - self.edit_collapsible.set_functions_curves(function_curve_list) + def setFunctionsCurves(self, function_curve_list: list): + self.editCollapsible.setFunctionsCurves(function_curve_list) - def set_current_function_curve_data(self, curve_data): - self.edit_collapsible.set_current_function_curve_data(curve_data) + def setCurrentFunctionCurveData(self, curve_data): + self.editCollapsible.setCurrentFunctionCurveData(curve_data) - def __on_function_curve_selected(self, function_curve): - self.edit_collapsible.set_current_function_curve_data(None) - for fit_data in self.__fit_data_list: - if fit_data.name == function_curve: - self.edit_collapsible.set_current_function_curve_data(fit_data) + def __onFunctionCurveSelected(self, function_curve): + self.editCollapsible.setCurrentFunctionCurveData(None) + for fitData in self.__fitDataList: + if fitData.name == function_curve: + self.editCollapsible.setCurrentFunctionCurveData(fitData) break - self.__update_output_button_state() + self.__updateOutputButtonState() - def __on_create_clicked(self): - function_curve_name = self.__functionCurveNameLineEdit.text - if function_curve_name == "": + def __onCreateClicked(self): + functionCurveName = self.__functionCurveNameLineEdit.text + if functionCurveName == "": return - for fit_equation in self.__fit_equations: - if fit_equation.widget.DISPLAY_NAME == self.__fit_equation_combo_box.currentText: - fit_data = fit_equation.equation.create_default(function_curve_name) - self.signal_new_function_curve_data.emit(fit_data) + for fitEquation in self.__fitEquations: + if fitEquation.widget.DISPLAY_NAME == self.__fitEquationComboBox.currentText: + fitData = fitEquation.equation.create_default(functionCurveName) + self.signalNewFunctionCurveData.emit(fitData) break - def __on_import_clicked(self): - self.signal_import_function_curve.emit() + def __onImportClicked(self): + self.signalImportFunctionCurve.emit() - def __on_export_clicked(self): - function_curve_name = self.edit_collapsible.get_current_function_curve_name() - self.signal_export_function_curve.emit(function_curve_name) + def __onExportClicked(self): + functionCurveName = self.editCollapsible.getCurrentFunctionCurveName() + self.signalExportFunctionCurve.emit(functionCurveName) - def __on_function_curve_edited(self, function_curve, parameter_name, new_value, is_fixed): - self.signal_function_curve_edited.emit(function_curve, parameter_name, new_value) + def __onFunctionCurveEdited(self, function_curve, parameterName, newValue, isFixed): + self.signalFunctionCurveEdited.emit(function_curve, parameterName, newValue) def __on_save_button_clicked(self): - function_curve_name = self.edit_collapsible.get_current_function_curve_name() - self.signal_save_data.emit(function_curve_name) - - def __update_output_button_state(self): - valid_function_curve = False - function_curve_name = self.edit_collapsible.get_current_function_curve_name() - if function_curve_name: - for fit_data in self.__fit_data_list: - if fit_data.name == function_curve_name: - valid_function_curve = True + functionCurveName = self.editCollapsible.getCurrentFunctionCurveName() + self.signalSaveData.emit(functionCurveName) + + def __updateOutputButtonState(self): + validFunctionCurve = False + functionCurveName = self.editCollapsible.getCurrentFunctionCurveName() + if functionCurveName: + for fitData in self.__fitDataList: + if fitData.name == functionCurveName: + validFunctionCurve = True break - self.__save_button.enabled = valid_function_curve - self.__exportButton.enabled = valid_function_curve + self.__saveButton.enabled = validFunctionCurve + self.__exportButton.enabled = validFunctionCurve class ShrinkableStackedWidget(qt.QFrame): @@ -1357,9 +1388,9 @@ class ShrinkableStackedWidget(qt.QFrame): def __init__(self): super().__init__() self.widgetList = [] - self.main_layout = qt.QVBoxLayout() - self.main_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.main_layout) + self.mainLayout = qt.QVBoxLayout() + self.mainLayout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self.mainLayout) def setCurrentIndex(self, index): for i, widget in enumerate(self.widgetList): @@ -1370,4 +1401,4 @@ def setCurrentIndex(self, index): def addWidget(self, widget): self.widgetList.append(widget) - self.main_layout.addWidget(widget) + self.mainLayout.addWidget(widget) diff --git a/src/modules/Charts/Plots/Crossplot/data_plot_widget.py b/src/modules/Charts/Plots/Crossplot/data_plot_widget.py index 658a7bc..624ebde 100644 --- a/src/modules/Charts/Plots/Crossplot/data_plot_widget.py +++ b/src/modules/Charts/Plots/Crossplot/data_plot_widget.py @@ -11,11 +11,14 @@ PLOT_HISTOGRAM_SIZE = 100 -class DataPlotWidget: +class DataPlotWidget(pg.QtCore.QObject): + toggleLegendSignal = pg.QtCore.Signal() + def __init__(self): super().__init__() self.__legendItem = None + self.__embeddedLegendVisibility = True self.__currentColorMap = None self.__input_data = {} self.__xHistogramPlots = list() @@ -41,6 +44,13 @@ def __init__(self): axisItems = {"bottom": axisBottom} self.__plotItem = self.__graphicsLayoutWidget.addPlot(row=1, col=2, colspan=2, rowspan=2, axisItems=axisItems) + + legendBorderPen = pg.mkPen(QtGui.QColor(0, 0, 0, 255), width=1) + legendBackgroundBrush = pg.mkBrush(QtGui.QColor(180, 180, 180, 100)) + self.__embeddedLegendItem = self.__plotItem.addLegend(brush=legendBackgroundBrush, pen=legendBorderPen) + self.__embeddedLegendItem.anchor(itemPos=(1, 0), parentPos=(1, 0), offset=(-10, 10)) + self.__embeddedLegendItem.setVisible(self.__embeddedLegendVisibility) + self.__plotItem.showGrid(x=True, y=True, alpha=0.8) self.__yHistogramPlotItem = self.__graphicsLayoutWidget.addPlot(row=1, col=0, rowspan=2) self.__xHistogramPlotItem = self.__graphicsLayoutWidget.addPlot(row=0, col=2, colspan=2) @@ -67,6 +77,18 @@ def plotItem(self): def themes(self): return self.__themeNames + @property + def embeddedLegendVisibility(self): + return self.__embeddedLegendVisibility + + @embeddedLegendVisibility.setter + def embeddedLegendVisibility(self, isVisible): + if self.__embeddedLegendVisibility == isVisible: + return + + self.__embeddedLegendVisibility = isVisible + self.__embeddedLegendItem.setVisible(self.__embeddedLegendVisibility) + def clear_histogram_x(self): self.__xHistogramPlotItem.clear() @@ -101,6 +123,7 @@ def add2dPlot(self, graphData, xData, yData, xAxisParameter, yAxisParameter): size=graphData.style.size, pen=pen, brush=brush, + name=graphData.name, ) self.__plotItem.addItem(spi) @@ -154,6 +177,7 @@ def add3dPlot(self, graphData, xData, yData, zData, xAxisParameter, yAxisParamet size=graphData.style.size, brush=brush, pen=pen, + name=graphData.name, ) self.__plotItem.addItem(spi) @@ -201,6 +225,7 @@ def add_curve_plot(self, curve_data): curve_data.y, stepMode=False, pen=pg.mkPen(curve_data.style.color, width=curve_data.style.line_size, style=curve_data.style.line_style), + name=curve_data.name, ) self.__plotItem.addItem(new_curve) @@ -345,6 +370,8 @@ def __removeLegendWidget(self): def __overwritePlotMenu(self): menu = self.__plotItem.ctrlMenu + + # Remove actions actions_to_remove = [] blackList = ["Transforms", "Downsample", "Average", "Alpha", "Points"] for action in menu.actions(): @@ -354,6 +381,13 @@ def __overwritePlotMenu(self): for action in actions_to_remove: menu.removeAction(action) + # Add new actions + menu.addAction("Toggle legend", self.__toggleLegend) + + def __toggleLegend(self): + self.embeddedLegendVisibility = not self.embeddedLegendVisibility + self.toggleLegendSignal.emit() + def __createGradientLegend(self, z, zMin, zMax): """Handles pg.GradientLegend and related objects creation based on the color map chosed.""" zNormalized = (z - zMin) / (zMax - zMin) diff --git a/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py b/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py index b8d514d..3832a2d 100644 --- a/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py +++ b/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py @@ -103,6 +103,10 @@ def setCurrentSegmentTransparent(self): Call restorePreviewedSegmentTransparency() to restore original opacity. """ + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -130,6 +134,10 @@ def setCurrentSegmentTransparent(self): def restorePreviewedSegmentTransparency(self): """Restore previewed segment's opacity that was temporarily made transparen by calling setCurrentSegmentTransparent().""" + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -467,19 +475,22 @@ def updateMRMLFromGUI(self): self.scriptedEffect.setParameter("ColorThresholdEffect.pulse", int(self.enablePulsingCheckbox.isChecked())) def _getColor(self): + color = [0.5, 0.5, 0.5] + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return color # Get color of edited segment segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: # scene was closed while preview was active - return + return color displayNode = segmentationNode.GetDisplayNode() if displayNode is None: logging.error("preview: Invalid segmentation display node!") - color = [0.5, 0.5, 0.5] segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() if segmentID is None: - return + return color # Make sure we keep the currently selected segment hidden (the user may have changed selection) if segmentID != self.previewedSegmentID: diff --git a/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py b/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py index ace3819..fb7113a 100644 --- a/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py +++ b/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py @@ -114,6 +114,9 @@ def createCursor(self, widget): return slicer.util.mainWindow().cursor def onHopsChanged(self, hops): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return sourceVolumeNode = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode() voxelArray = np.squeeze(slicer.util.arrayFromVolume(sourceVolumeNode)) if hops > voxelArray.ndim: @@ -126,6 +129,10 @@ def onDirectionChanged(self, direction): self.updateMRMLFromGUI() def getSegmentData(self, segmentationNode, sourceVolumeNode): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return + # Get color of edited segment if not segmentationNode: # scene was closed while preview was active @@ -211,13 +218,9 @@ def onApply(self): slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(outputVolume, segmentationNode) segmentation = segmentationNode.GetSegmentation() segmentation.GetNthSegment(segmentation.GetNumberOfSegments() - 1).SetName(outputSegmentName) - except IndexError as ier: - slicer.app.restoreOverrideCursor() - slicer.util.errorDisplay("Something went wrong.") - print(repr(ier)) - except ValueError as ver: + except Exception as error: slicer.app.restoreOverrideCursor() - slicer.util.errorDisplay(ver) + slicer.util.errorDisplay(f"Failed to apply the effect.\nError: {error}.") finally: helpers.removeTemporaryNodes() @@ -239,18 +242,22 @@ def updateMRMLFromGUI(self): self.scriptedEffect.setParameter("ConnectivityEffect.direction", self.directionChoice.currentText) def _getColor(self): + color = [0.5, 0.5, 0.5] # Get color of edited segment + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return color + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: # scene was closed while preview was active - return + return color displayNode = segmentationNode.GetDisplayNode() if displayNode is None: logging.error("preview: Invalid segmentation display node!") - color = [0.5, 0.5, 0.5] segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() if segmentID is None: - return + return color # Change color hue slightly to make it easier to distinguish filled regions from preview r, g, b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor() diff --git a/src/modules/CoreEnv/CoreEnv.py b/src/modules/CoreEnv/CoreEnv.py index db8b2b3..02789d7 100644 --- a/src/modules/CoreEnv/CoreEnv.py +++ b/src/modules/CoreEnv/CoreEnv.py @@ -39,8 +39,8 @@ def readme_path(cls): class CoreEnvWidget(LTracePluginWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, parent) -> None: + super().__init__(parent) self.lastAccessedWidget = None def setup(self): diff --git a/src/modules/CoreGeometryCLI/CoreGeometryCLI.py b/src/modules/CoreGeometryCLI/CoreGeometryCLI.py index c7c5e04..a1e5ec9 100644 --- a/src/modules/CoreGeometryCLI/CoreGeometryCLI.py +++ b/src/modules/CoreGeometryCLI/CoreGeometryCLI.py @@ -11,10 +11,8 @@ import numpy as np import slicer import slicer.util -from ltrace.wrappers import sanitize_file_path from ltrace.transforms import transformPoints, getRoundedInteger from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT -from pathvalidate.argparse import sanitize_filepath_arg from skimage.feature import canny from skimage.transform import hough_circle, hough_circle_peaks @@ -41,7 +39,7 @@ def calculateCoreGeometry(volume, coreRadius): manager = Manager() slicesCoreGeometryInIJKCoordinates = manager.list() - pool = Pool(max(cpu_count() - 2, 1)) + pool = Pool(min(max(cpu_count() - 2, 1), 4)) parameters = [] for i in np.linspace(startSlice, endSlice, num=numSliceSamples, dtype=int): parameters.append((i, normalizedVolumeArray[i], searchRadius, slicesCoreGeometryInIJKCoordinates)) @@ -77,7 +75,7 @@ def calculateSliceCoreGeometry(parameters): def findSmallestCircle(edges, startRadius, endRadius, totalNumPeaks): - houghRadii = np.arange(startRadius, endRadius, 1) + houghRadii = np.arange(startRadius, endRadius, 2) houghRes = hough_circle(edges, houghRadii) # Select the most prominent circles _, cx, cy, radii = hough_circle_peaks(houghRes, houghRadii, num_peaks=1, total_num_peaks=totalNumPeaks) @@ -115,7 +113,7 @@ def getSpacing(node): parser = argparse.ArgumentParser() parser.add_argument("volume", type=str) parser.add_argument("coreRadius", type=float) - parser.add_argument("coreGeometryDataFile", type=str, type=sanitize_filepath_arg) + parser.add_argument("coreGeometryDataFile", type=str) args = parser.parse_args() # Loading the volume nrrd file from disk @@ -126,8 +124,7 @@ def getSpacing(node): # Calculating the core geometry (core centers and radii) coreGeometryData = calculateCoreGeometry(volume, args.coreRadius) - coreGeometryDataFile = sanitize_file_path(args.coreGeometryDataFile) # Saving result on disk - with open(coreGeometryDataFile.as_posix(), "wb") as f: + with open(Path(args.coreGeometryDataFile).absolute(), "wb") as f: f.write(pickle.dumps(coreGeometryData)) diff --git a/src/modules/CoreImagesImport/CoreImagesImport.py b/src/modules/CoreImagesImport/CoreImagesImport.py index 7933e69..6cb1418 100644 --- a/src/modules/CoreImagesImport/CoreImagesImport.py +++ b/src/modules/CoreImagesImport/CoreImagesImport.py @@ -44,8 +44,8 @@ def get_instance(): class CoreImagesImportWidget(LTracePluginWidget): - def __init__(self, *args, **kwargs): - LTracePluginWidget.__init__(self, *args, **kwargs) + def __init__(self, parent) -> None: + LTracePluginWidget.__init__(self, parent) initial_depth_frame = qt.QWidget() initial_depth_frame.setLayout(qt.QFormLayout()) @@ -125,9 +125,6 @@ def __init__(self, *args, **kwargs): def setup(self): LTracePluginWidget.setup(self) - def cleanup(self): - pass - def _on_add_clicked(self): last_path = slicer.app.settings().value("CoreImagesImport/last-load-path") if last_path is None: diff --git a/src/modules/CoreInpaint/CoreInpaint.py b/src/modules/CoreInpaint/CoreInpaint.py index 34178e7..0a2c68e 100644 --- a/src/modules/CoreInpaint/CoreInpaint.py +++ b/src/modules/CoreInpaint/CoreInpaint.py @@ -9,6 +9,11 @@ from pathlib import Path from Libs.patchmatch import PatchMatch +try: + from Test.CoreInpaintTest import CoreInpaintTest +except ImportError: + CoreInpaintTest = None # tests not deployed to final version or closed source + class CoreInpaint(LTracePlugin): SETTING_KEY = "CoreInpaint" @@ -38,7 +43,7 @@ def setup(self): hideSoi=True, hideCalcProp=True, allowedInputNodes=["vtkMRMLSegmentationNode"] ) - self.inputWidget.onReferenceSelected = self.onReferenceSelected + self.inputWidget.onReferenceSelectedSignal.connect(self.onReferenceSelected) self.outputNameField = qt.QLineEdit() self.outputNameField.setToolTip("Name of the output volume") diff --git a/src/modules/CoreInpaint/Libs/patchmatch.py b/src/modules/CoreInpaint/Libs/patchmatch.py index d9d0e12..b95ae2b 100644 --- a/src/modules/CoreInpaint/Libs/patchmatch.py +++ b/src/modules/CoreInpaint/Libs/patchmatch.py @@ -322,7 +322,7 @@ def iterate( debug_score = 0.0 debug_changed = 0 debug_propagated = 0 - n_offsets = len(offsets_to_visit) + n_offsets = max(len(offsets_to_visit), self.callback_every) if reverse: offsets_to_visit = reversed(offsets_to_visit) @@ -401,14 +401,15 @@ def iterate( self.progress.end_step() - print( - debug_score / (n_offsets * self.patch_size**3), - debug_propagated, - "/", - debug_changed, - "/", - n_offsets, - ) + # TODO: Only print this in debug mode + # print( + # debug_score / (n_offsets * self.patch_size**3), + # debug_propagated, + # "/", + # debug_changed, + # "/", + # n_offsets, + # ) return image diff --git a/src/modules/CorePhotographLoader/CorePhotographLoaderCLI/CorePhotographLoaderCLI.py b/src/modules/CorePhotographLoader/CorePhotographLoaderCLI/CorePhotographLoaderCLI.py index 936512b..bbd5426 100644 --- a/src/modules/CorePhotographLoader/CorePhotographLoaderCLI/CorePhotographLoaderCLI.py +++ b/src/modules/CorePhotographLoader/CorePhotographLoaderCLI/CorePhotographLoaderCLI.py @@ -8,7 +8,6 @@ import pandas as pd import mrml import json -import logging import pytesseract import sys import traceback @@ -61,7 +60,8 @@ def main(args): exit_code = 0 except Exception as error: - logging.error(f"{repr(error)}\n{traceback.print_exc()}") + print(traceback.format_exc()) + print(str(error), file=sys.stderr) exit_code = 1 return exit_code diff --git a/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py b/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py index 810f708..aee57d8 100644 --- a/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py +++ b/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py @@ -292,7 +292,9 @@ def __outputWidgetSetup(self): return outputCollapsibleButton def cleanup(self): - pass + super().cleanup() + for node, tag in self.registered_callbacks: + node.RemoveObserver(tag) def __onNodeSelectionChanged(self): """Handles input/output node selection change event""" diff --git a/src/modules/CustomizedCropVolume/CustomizedCropVolume.py b/src/modules/CustomizedCropVolume/CustomizedCropVolume.py index 0f13b03..b0d72c8 100644 --- a/src/modules/CustomizedCropVolume/CustomizedCropVolume.py +++ b/src/modules/CustomizedCropVolume/CustomizedCropVolume.py @@ -9,6 +9,7 @@ from ltrace.slicer.helpers import bounds2size, copy_display from ltrace.slicer_utils import * +from ltrace.slicer.node_observer import NodeObserver try: from Test.CustomizedCropVolumeTest import CustomizedCropTest @@ -38,6 +39,7 @@ def readme_path(cls): class CustomizedCropVolumeWidget(LTracePluginWidget): def __init__(self, parent): LTracePluginWidget.__init__(self, parent) + self.roiObserver = None def setup(self): LTracePluginWidget.setup(self) @@ -143,14 +145,14 @@ def sizeBoxHasFocus(self): def currentNodeChanged(self): volume = self.volumeComboBox.currentNode() - if volume: + if volume and volume.GetImageData() is not None: dims = volume.GetImageData().GetDimensions() for dim, sizeBox in zip(dims, self.sizeBoxes): sizeBox.setRange(0, dim) + sizeBox.setValue(dim) self.sizeEditWidget.setVisible(True) else: self.sizeEditWidget.setVisible(False) - self.logic.initializeVolume(volume) def onCropButtonClicked(self): @@ -199,14 +201,23 @@ def onSizeBoxChanged(self, value): def enter(self) -> None: super().enter() + if self.roiObserver is not None: + return self.volumeComboBox.setCurrentNode(None) self.logic.roi = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLMarkupsROINode.__name__, "Crop ROI") self.logic.roi.SetDisplayVisibility(False) self.logic.roi.GetDisplayNode().SetFillOpacity(0.5) - self.logic.roi.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onRoiModified) + self.roiObserver = NodeObserver(node=self.logic.roi, parent=None) + self.roiObserver.modifiedSignal.connect(self.onRoiModified) def exit(self): slicer.mrmlScene.RemoveNode(self.logic.roi) + if self.roiObserver is None: + return + + self.roiObserver.clear() + del self.roiObserver + self.roiObserver = None def updateStatus(self, message, progress=None, processEvents=True): self.progressBar.show() @@ -226,6 +237,10 @@ def updateStatus(self, message, progress=None, processEvents=True): with self.progressMux: slicer.app.processEvents() + def cleanup(self): + super().cleanup() + self.exit() + class Callback(object): def __init__(self, on_update=None): @@ -239,7 +254,7 @@ def __init__(self): self.cropVolumeNode = None def initializeVolume(self, volume): - if volume is not None: + if volume is not None and self.roi is not None: cropVolumeParameters = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLCropVolumeParametersNode.__name__) cropVolumeParameters.SetIsotropicResampling(True) cropVolumeParameters.SetInputVolumeNodeID(volume.GetID()) @@ -248,7 +263,7 @@ def initializeVolume(self, volume): self.roi.SetDisplayVisibility(True) slicer.util.setSliceViewerLayers(foreground=None, background=volume, label=None, fit=True) self.cropVolumeNode = cropVolumeParameters - else: + elif self.roi is not None: self.roi.SetDisplayVisibility(False) def crop(self, volume, ijkSize): diff --git a/src/modules/CustomizedData/CustomizedData.py b/src/modules/CustomizedData/CustomizedData.py index 741f1da..5f45198 100644 --- a/src/modules/CustomizedData/CustomizedData.py +++ b/src/modules/CustomizedData/CustomizedData.py @@ -68,7 +68,7 @@ def setup(self): # Adds confirmation step before delete action nodeMenu = self.subjectHierarchyTreeView.findChild(qt.QMenu, "nodeMenuTreeView") - deleteAction = nodeMenu.actions()[3] # Delete + self.deleteAction = nodeMenu.actions()[3] # Delete def confirmDeleteSelectedItems(): message = "Are you sure you want to delete the selected nodes?" @@ -80,14 +80,16 @@ def confirmDeleteSelectedItems(): ) self.subjectHierarchyTreeView.deleteSelectedItems() - deleteAction.triggered.disconnect() - deleteAction.triggered.connect(confirmDeleteSelectedItems) + self.deleteAction.triggered.disconnect() + self.deleteAction.triggered.connect(confirmDeleteSelectedItems) # hack to workaround currentItemChanged firing twice self.subjectHierarchyTreeView.currentItemsChanged.connect(self.currentItemChanged) # Add observer - slicer.mrmlScene.AddObserver(slicer.mrmlScene.EndCloseEvent, lambda *args: self.currentItemChanged(None)) + self.endSceneObserver = slicer.mrmlScene.AddObserver( + slicer.mrmlScene.EndCloseEvent, lambda *args: self.currentItemChanged(None) + ) self.subjectHierarchyTreeView.setMinimumHeight(310) self.subjectHierarchyTreeView.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum) @@ -147,3 +149,10 @@ def currentItemChanged(self, itemID_bogus): except Exception as error: logging.info(f"{error}\n{traceback.print_exc()}") pass + + def cleanup(self): + super().cleanup() + self.subjectHierarchyTreeView.currentItemsChanged.disconnect() + slicer.mrmlScene.RemoveObserver(self.endSceneObserver) + self.deleteAction.triggered.disconnect() + self.scalarVolumeWidget.cleanup() diff --git a/src/modules/CustomizedData/CustomizedDataLib/ScalarVolume.py b/src/modules/CustomizedData/CustomizedDataLib/ScalarVolume.py index 2f200b0..4a1ab4e 100644 --- a/src/modules/CustomizedData/CustomizedDataLib/ScalarVolume.py +++ b/src/modules/CustomizedData/CustomizedDataLib/ScalarVolume.py @@ -6,13 +6,47 @@ from ltrace.slicer.helpers import ( setVolumeVisibilityIn3D, getVolumeVisibilityIn3D, - setSlicesVisibilityIn3D, getScalarTypesAsString, + BlockSignals, ) from ltrace.slicer.node_attributes import ColorMapSelectable from ltrace.slicer.helpers import tryGetNode +def setSlicesVisibilityIn3D(volume, visible): + if visible: + if volume.IsA("vtkMRMLLabelMapVolumeNode"): + slicer.util.setSliceViewerLayers(label=volume, fit=True) + else: + slicer.util.setSliceViewerLayers(background=volume, fit=True) + for sliceViewLabel in ["Red", "Yellow", "Green"]: + sliceView = slicer.app.layoutManager().sliceWidget(sliceViewLabel).sliceView() + sliceView.mrmlSliceNode().SetSliceVisible(visible) + + +def getSlicesVisibilityIn3D(volume): + volumeVisibilities = [] + sliceVisibilities = [] + for sliceViewLabel in ["Red", "Yellow", "Green"]: + sliceWidget = slicer.app.layoutManager().sliceWidget(sliceViewLabel) + sliceView = sliceWidget.sliceView() + sliceVisibilities.append(sliceView.mrmlSliceNode().GetSliceVisible()) + + compositeNode = sliceWidget.mrmlSliceCompositeNode() + + if volume.IsA("vtkMRMLLabelMapVolumeNode"): + sliceVolumeID = compositeNode.GetLabelVolumeID() + else: + sliceVolumeID = compositeNode.GetBackgroundVolumeID() + + volumeVisibilities.append(sliceVolumeID == volume.GetID()) + + volumeIsVisible = all(volumeVisibilities) + slicesAreVisible = all(sliceVisibilities) + + return volumeIsVisible and slicesAreVisible + + class ScalarVolumeWidget(qt.QWidget): def __init__(self, isLabelMap, *args, **kwargs): super().__init__(*args, **kwargs) @@ -26,14 +60,16 @@ def setup(self): contentsFrameLayout.setLabelAlignment(qt.Qt.AlignRight) contentsFrameLayout.setContentsMargins(0, 0, 0, 0) - volumesWidget = slicer.modules.volumes.createNewWidgetRepresentation() + self.volumesWidget = slicer.modules.volumes.createNewWidgetRepresentation() self.sequenceModule = slicer.modules.sequences.createNewWidgetRepresentation() - volumeDisplayWidget = volumesWidget.findChild( + volumeDisplayWidget = self.volumesWidget.findChild( slicer.qSlicerScalarVolumeDisplayWidget, "qSlicerScalarVolumeDisplayWidget" ) - self.activeVolumeNodeSelector = volumesWidget.findChild(slicer.qMRMLNodeComboBox, "ActiveVolumeNodeSelector") + self.activeVolumeNodeSelector = self.volumesWidget.findChild( + slicer.qMRMLNodeComboBox, "ActiveVolumeNodeSelector" + ) imageDimensionsHBoxLayout = qt.QHBoxLayout() self.imageDimensions1LineEdit = qt.QLineEdit() @@ -82,8 +118,9 @@ def setup(self): ) self.slicesIn3DCheckBox = qt.QCheckBox("Slices in 3D") - self.slicesIn3DCheckBox.setChecked(True) - self.slicesIn3DCheckBox.stateChanged.connect(lambda state: setSlicesVisibilityIn3D(state == qt.Qt.Checked)) + self.slicesIn3DCheckBox.stateChanged.connect( + lambda state: setSlicesVisibilityIn3D(self.node, state == qt.Qt.Checked) if self.node else None + ) checkBoxLayout.addWidget(self.renderIn3DCheckBox, 1) checkBoxLayout.addWidget(self.slicesIn3DCheckBox, 1) @@ -160,6 +197,8 @@ def setNode(self, node): self.node = node self.activeVolumeNodeSelector.setCurrentNode(node) self.renderIn3DCheckBox.setChecked(getVolumeVisibilityIn3D(node)) + with BlockSignals(self.slicesIn3DCheckBox): + self.slicesIn3DCheckBox.setChecked(getSlicesVisibilityIn3D(node)) browser_node = slicer.modules.sequences.logic().GetFirstBrowserNodeForProxyNode(node) if browser_node: @@ -206,3 +245,6 @@ def update(self): self.sequenceModule.setActiveBrowserNode(None) self.sequenceBrowserContainerWidget.hide() self.sequenceLabel.hide() + + def cleanup(self): + self.volumesWidget.delete() diff --git a/src/modules/CustomizedData/CustomizedDataLib/Segmentation.py b/src/modules/CustomizedData/CustomizedDataLib/Segmentation.py index 015114d..ebef7e4 100644 --- a/src/modules/CustomizedData/CustomizedDataLib/Segmentation.py +++ b/src/modules/CustomizedData/CustomizedDataLib/Segmentation.py @@ -20,7 +20,25 @@ def setup(self): contentsFrameLayout.addRow(self.segmentsTableView) self.show3DButton = slicer.qMRMLSegmentationShow3DButton() contentsFrameLayout.addRow(self.show3DButton) + self.opacitySlider = slicer.qMRMLSliderWidget() + self.opacitySlider.maximum = 1 + self.opacitySlider.minimum = 0 + self.opacitySlider.value = 0.5 + self.opacitySlider.singleStep = 0.05 + self.opacitySlider.valueChanged.connect(lambda value: self.onOverallOpacityChanged(value)) + self.opacitySlider.toolTip = """\ + This parameter controls the overall opacity in all views of that segmentation.\ + """ + contentsFrameLayout.addRow("Overall Opacity:", self.opacitySlider) + + def onOverallOpacityChanged(self, value): + segmentationNode = self.show3DButton.segmentationNode() + segmentationDisplayNode = segmentationNode.GetDisplayNode() + segmentationDisplayNode.SetOpacity(value) def setNode(self, node): self.segmentsTableView.setSegmentationNode(node) self.show3DButton.setSegmentationNode(node) + segmentationDisplayNode = node.GetDisplayNode() + if segmentationDisplayNode: + self.opacitySlider.value = segmentationDisplayNode.GetOpacity() diff --git a/src/modules/CustomizedData/CustomizedDataLib/VectorVolume.py b/src/modules/CustomizedData/CustomizedDataLib/VectorVolume.py index e315d01..652e6d5 100644 --- a/src/modules/CustomizedData/CustomizedDataLib/VectorVolume.py +++ b/src/modules/CustomizedData/CustomizedDataLib/VectorVolume.py @@ -19,6 +19,8 @@ def setup(self): contentsFrameLayout.setContentsMargins(0, 0, 0, 0) volumesWidget = slicer.modules.volumes.createNewWidgetRepresentation() + volumesWidget.setParent(self) + volumesWidget.hide() volumeDisplayWidget = volumesWidget.findChild( slicer.qSlicerScalarVolumeDisplayWidget, "qSlicerScalarVolumeDisplayWidget" @@ -107,7 +109,7 @@ def setNode(self, node): self.windowLevelWidget.setAutoWindowLevel(previousAutoWindowLevel) self.update() - def update(self): + def update(self, *args, **kwargs): if self.node is not None: spacing = [f"{spacing:.10g}" for spacing in self.node.GetSpacing()] imageDimensions = self.node.GetImageData().GetDimensions() diff --git a/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py b/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py index 6cdf392..9920d31 100644 --- a/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py +++ b/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py @@ -303,6 +303,12 @@ def onApplyButtonClicked(self): def onCancelButtonClicked(self): self.logic.cancel() + def cleanup(self): + super().cleanup() + self.logic.filteringStarted.disconnect() + self.logic.filteringStopped.disconnect() + self.logic.filteringCompleted.disconnect() + class CustomizedGradientAnisotropicDiffusionLogic(LTracePluginLogic): filteringStarted = qt.Signal() diff --git a/src/modules/CustomizedSegmentEditor/CustomizedEffects/Threshold/SegmentEditorThresholdEffect.py b/src/modules/CustomizedSegmentEditor/CustomizedEffects/Threshold/SegmentEditorThresholdEffect.py index 081658d..22783fe 100644 --- a/src/modules/CustomizedSegmentEditor/CustomizedEffects/Threshold/SegmentEditorThresholdEffect.py +++ b/src/modules/CustomizedSegmentEditor/CustomizedEffects/Threshold/SegmentEditorThresholdEffect.py @@ -111,6 +111,10 @@ def setCurrentSegmentTransparent(self): Call restorePreviewedSegmentTransparency() to restore original opacity. """ + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -138,6 +142,10 @@ def setCurrentSegmentTransparent(self): def restorePreviewedSegmentTransparency(self): """Restore previewed segment's opacity that was temporarily made transparen by calling setCurrentSegmentTransparent().""" + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("The segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: return @@ -527,6 +535,10 @@ def onThresholdValuesChanged(self,min,max): def onUseForPaint(self): parameterSetNode = self.scriptedEffect.parameterSetNode() + if parameterSetNode is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + parameterSetNode.SourceVolumeIntensityMaskOn() parameterSetNode.SetSourceVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue) # Switch to paint effect @@ -638,6 +650,9 @@ def onApply(self): # De-select effect self.scriptedEffect.selectEffect("") + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: @@ -696,6 +711,10 @@ def preview(self): min = self.scriptedEffect.doubleParameter("MinimumThreshold") max = self.scriptedEffect.doubleParameter("MaximumThreshold") + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + # Get color of edited segment segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: diff --git a/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py b/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py index 0404be5..97e6c79 100644 --- a/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py +++ b/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py @@ -291,5 +291,8 @@ def onSceneEndImport(self, caller, event): self.editor.updateWidgetFromMRML() def cleanup(self): + super().cleanup() self.removeObservers() self.deactivateEditorRegisteredCallback() + self.__updateEffectRegisteredTimer.timeout.disconnect() + self.__applicationObservables.applicationLoadFinished.disconnect() diff --git a/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py b/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py index 67fd6cb..92939c3 100644 --- a/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py +++ b/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py @@ -1,21 +1,21 @@ # SegmentEditorSmoothingEffect.py # Copied and modified from 3D Slicer -import logging -import os import ctk import qt import vtk - import slicer +import logging import numpy as np -from scipy import ndimage -from vtk.util import numpy_support +import os +import traceback +from ltrace.slicer.helpers import getSourceVolume from pathlib import Path +from scipy import ndimage from SegmentEditorEffects import * -from ltrace.slicer.helpers import getSourceVolume +from vtk.util import numpy_support class SegmentEditorSmoothingEffect(AbstractScriptedSegmentEditorPaintEffect): @@ -253,7 +253,9 @@ def onApply(self, maskImage=None, maskExtent=None): # This can be a long operation - indicate it to the user qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) self.scriptedEffect.saveStateForUndo() - + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return if smoothingMethod == JOINT_TAUBIN: self.smoothMultipleSegments(maskImage, maskExtent) elif applyToAllVisibleSegments: @@ -527,12 +529,16 @@ def smoothSelectedSegment(self, maskImage=None, maskExtent=None): smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent ) - except IndexError: - logging.error("apply: Failed to apply smoothing") + except Exception as error: + logging.debug(f"Error: {error}. Traceback:\n{traceback.format_exc()}") def smoothMultipleSegments(self, maskImage=None, maskExtent=None): import vtkSegmentationCorePython as vtkSegmentationCore + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply smoothing. The selected node is not valid.") + return + self.showStatusMessage(f"Joint smoothing ...") # Generate merged labelmap of all visible segments segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() diff --git a/src/modules/DLISImport/DLISImport.py b/src/modules/DLISImport/DLISImport.py index ea6d846..7a51000 100644 --- a/src/modules/DLISImport/DLISImport.py +++ b/src/modules/DLISImport/DLISImport.py @@ -149,8 +149,8 @@ def _conversion_factor_to_millimeters(self, unit): class DLISImportWidget(LTracePluginWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, parent) -> None: + super().__init__(parent) self.logic = DLISImportLogic() def setup(self): @@ -174,9 +174,6 @@ def setup(self): self.reload_last_button.setEnabled(self._get_last_load_options() is not None) self.layout.addWidget(self.reload_last_button) - def cleanup(self): - pass - def _on_load_clicked(self, mnemonic_and_files): well_diameter = float(self.dlis_widget.wellDiameter.text) * 25.4 # inches to mm self._set_last_load_options((self.dlis_widget.currentPath(), mnemonic_and_files, well_diameter)) diff --git a/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py b/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py index 4bf74ae..8fae532 100644 --- a/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py +++ b/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py @@ -31,12 +31,26 @@ NULL_VALUE_TAG = "NullValue" LOGICAL_FILE_TAG = "LogicalFile" FRAME_TAG = "Frame" +WELL_NAME_TAG = "WellName" +UNITS_TAG = "Units" DEPTH_LABEL = "DEPTH" CURVES_NAME = ["T2_DIST", "T2DIST", "T1DIST"] ChannelMetadata = namedtuple( - "ChannelMetadata", ["mnemonic", "name", "unit", "frame_name", "logical_file", "is_labelmap", "is_table"] + "ChannelMetadata", + [ + "mnemonic", + "name", + "unit", + "frame_name", + "logical_file", + "is_labelmap", + "is_table", + "stacked_curves", # For LAS files that contain curves to be stacked into images + ], ) +LAS_DEPTH_TAGS = ["DEPT", "DEPTH"] + class DLISLoader(object): def __init__(self, filepath): @@ -58,6 +72,8 @@ def load_metadata(self): values_db = [] well_name = "" for f in self.logical_files: + if len(f.origins) > 1: + logging.warning(f"File contains {len(f.origins)} origins") for o in f.origins: well_name = o.well_name for channel in f.channels: @@ -65,7 +81,7 @@ def load_metadata(self): continue framename = channel.frame.name - is_table = self.check_as_table(channel.name) + is_table = check_as_table(channel.name) values_db.append( ChannelMetadata( channel.name, @@ -75,6 +91,7 @@ def load_metadata(self): f.fileheader.id, False, is_table, + False, ) ) @@ -100,7 +117,7 @@ def load_data(self, file_path, mnemonic_and_files): continue framename = c.frame.name - if framename != metadata.frame_name or c.units != metadata.unit: + if framename != metadata.frame_name or (str(c.units) != str(metadata.unit)): continue image = c.curves() @@ -137,28 +154,25 @@ def load_data(self, file_path, mnemonic_and_files): c.units, ) - def check_as_table(self, channel_name): - valid_channels = [name in channel_name for name in CURVES_NAME] - return any(valid_channels) - class LASLoader(object): def __init__(self, filepath): self.filepath = filepath self.logical_files = lasio.read(str(self.filepath)) self.null_value = set([self.logical_files.well.NULL.value]) + self.stacked_curves_pattern = re.compile(r"(.+)[\[|(|{]([0-9]+)[\]|)|}]") - def load_volumes(self, curves, stepCallback, appFolder, nullValue, well_diameter_mm=None): + def load_volumes(self, curves, stepCallback, appFolder, nullValue, well_diameter_mm=None, well_name=None): if self.logical_files is None: return [] - well_name = self.find_wellname() - return load_volumes_as_table( + return load_volumes( curves=curves, stepCallback=stepCallback, appFolder=appFolder, nullValue=nullValue, - name=well_name, + well_diameter_mm=well_diameter_mm, + well_name=well_name, ) def clean(self): @@ -174,7 +188,7 @@ def find_wellname(self): else: well_name = Path(self.filepath).stem except Exception as e: - logging.error(f"An error occurred while obtaining the Well name: {repr(e)}") + print(f"An error occurred while obtaining the Well name: {e}") return well_name @@ -185,7 +199,10 @@ def load_metadata(self): well_name = self.find_wellname() values_db = [] - for curve in self.logical_files.curves: + + last_curve_name = "" + + for i, curve in enumerate(self.logical_files.curves): values_db.append( ChannelMetadata( curve.mnemonic, @@ -194,58 +211,105 @@ def load_metadata(self): "", well_name, "", - "", + is_table=check_as_table(curve.original_mnemonic), + stacked_curves=False, ) ) + + # Removing any number enclosed by []'s + + mnemo_search = self.stacked_curves_pattern.search(curve.mnemonic) + + mnemonic = original_mnemonic = "" + + if mnemo_search is None: + mnemonic = curve.mnemonic + else: + mnemonic = mnemo_search.group( + len(mnemo_search.groups()) - 1 + ) # Same as in check_image_data: find mnemonic with enclosed number, e.g. UBI_AMP[0] or UBI_AMP [dB][0] + + if mnemonic == last_curve_name: + values_db[i - 1] = values_db[i - 1]._replace(stacked_curves=True) + values_db[i] = values_db[i]._replace(stacked_curves=True) + # As stacked curves create 2D images, exporting as labelmap should now be possible. So, we change "" for False (otherwise the checkbox is disabled in our UI's table): + values_db[i - 1] = values_db[i - 1]._replace(is_labelmap=False) + values_db[i] = values_db[i]._replace(is_labelmap=False) + else: + last_curve_name = mnemonic + return well_name, values_db def load_data(self, file_path, mnemonic_and_files): if self.logical_files is None: raise ValueError("Missing LAS file.") + loaded_nodes_ids = [] + item_id = None + filename = slicer.mrmlScene.GetUniqueNameByString(Path(file_path).stem) well_name = self.find_wellname() - is_labelmap = False - is_table = False - for mf in mnemonic_and_files: + frame = "" + + depth_mnemonic = "" + + stacking = False + last_curve_mnemonic = "" + for i, mf in enumerate(mnemonic_and_files): curve = self.logical_files.curves[mf.mnemonic] + # the depth curve will be set as the domain of the other curves, not as a standalone curve + if curve.mnemonic in LAS_DEPTH_TAGS: + continue + try: domain = self.logical_files.depth_m except lasio.exceptions.LASUnknownUnitError: + logging.debug( + "Exception while setting domain of curve " + str(curve.mnemonic) + ". Trying from keys..." + ) for key in ("DEPT", "DEPTH", 0): try: domain = self.logical_files[key] + logging.debug("Domain set from key " + str(key)) break except KeyError: continue domain = domain * conversion_factor_to_millimeters("m") + image = curve.data + name = curve.mnemonic + units = curve.unit + + last_curve_mnemonic, is_same_mnemonic = checkMnemonicChanged(curve.mnemonic, last_curve_mnemonic) + + if not is_same_mnemonic: + stacking = False + if mf.stacked_curves: + if stacking == False: + image = self.logical_files.stack_curves(last_curve_mnemonic) + name = last_curve_mnemonic + stacking = True + else: + continue + else: + stacking = False + + name = f"{name}[{units}]" + yield ( filename, well_name, - curve.mnemonic, + name, # curve.mnemonic, "", domain, image, False, - is_labelmap, - is_table, - curve.unit, - ) - yield ( - filename, - well_name, - DEPTH_LABEL, - "", - domain, - None, - True, - is_labelmap, - is_table, - "mm", + mf.is_labelmap, + mf.is_table, + units, ) @@ -280,6 +344,7 @@ def load_volumes(self, curves, stepCallback, appFolder, nullValue, well_diameter appFolder=appFolder, nullValue=nullValue, name=self.filename, + well_name=well_name, ) def clean(self): @@ -346,7 +411,7 @@ def load_metadata(self): fake_mnemonic = f"Column {i+1}" self.db[fake_mnemonic] = ( - ChannelMetadata(fake_mnemonic, column, unit, "", self.filename, "", ""), + ChannelMetadata(fake_mnemonic, column, unit, "", self.filename, "", "", False), data, ) self.loaded_as_image = False @@ -356,13 +421,7 @@ def load_metadata(self): fake_mnemonic = f"Image" self.db[fake_mnemonic] = ( ChannelMetadata( - fake_mnemonic, - df.columns[1].replace("[0]", ""), - "", - "", - self.filename, - False, - False, + fake_mnemonic, df.columns[1].replace("[0]", ""), "", "", self.filename, "", False, False ), df.iloc[:, 1:].to_numpy(), ) @@ -434,6 +493,34 @@ class LoaderError(RuntimeError): pass +def checkMnemonicChanged(curr_mnemonic, last_curve_mnemonic): + stacked_curves_pattern = re.compile(r"(.+)[\[|(|{]([0-9]+)[\]|)|}]") + mnemo_search = stacked_curves_pattern.search(curr_mnemonic) + mnemonic = "" + is_same_mnemonic = False + + if mnemo_search is None: + mnemonic = curr_mnemonic + else: + mnemonic = mnemo_search.group( + len(mnemo_search.groups()) - 1 + ) # Same as in check_image_data: find mnemonic with enclosed number, e.g. UBI_AMP[0] or UBI_AMP [dB][0] + + if mnemonic == last_curve_mnemonic: + is_same_mnemonic = True + else: + last_curve_mnemonic = mnemonic + + return last_curve_mnemonic, is_same_mnemonic + + +def check_as_table(channel_name): + for name in CURVES_NAME: + if name in channel_name: + return True + return False + + def load_volumes_with_depth(curves, stepCallback, appFolder=None, nullValue=None, well_diameter_mm=310): loaded_nodes_ids = [] loaded_nodes = [] @@ -457,7 +544,7 @@ def load_volumes_with_depth(curves, stepCallback, appFolder=None, nullValue=None return loaded_nodes_ids -def load_volumes_as_table(curves, stepCallback=None, appFolder=None, nullValue=None, name="curves"): +def load_volumes_as_table(curves, stepCallback=None, appFolder=None, nullValue=None, name="curves", well_name=""): """Load the selected curves' data as a table node Args: @@ -477,14 +564,14 @@ def load_volumes_as_table(curves, stepCallback=None, appFolder=None, nullValue=N root_folder = curve[0] folder = curve[1] frame = curve[3] - units.append(str(curve[8])) + units.append(str(curve[9])) curve_name = curve[2] if curve_name == DEPTH_LABEL: curve_data = curve[4] else: curve_data = curve[5] - units.append(str(curve[8])) + units.append(str(curve[9])) curves_data[curve_name] = curve_data @@ -497,6 +584,7 @@ def load_volumes_as_table(curves, stepCallback=None, appFolder=None, nullValue=N name=name, null_value=nullValue, units=units, + well_name=well_name, ) return [] @@ -577,8 +665,9 @@ def add_volume( app_folder=app_folder, name=name, null_value=null_value, + well_name=well_name, ) - else: + else: # 1D (whether is_table is True or False) table_node, table_item_id = create_depth_curves_table( curves=curve_data, root_folder=top_folder, @@ -588,6 +677,7 @@ def add_volume( app_folder=app_folder, name=name, null_value=null_value, + well_name=well_name, ) return table_node, None @@ -672,22 +762,27 @@ def add_volume_from_data( return None, None # IF a TDEP exists into this frame, ignore a new one if is_labelmap: - volume_node = slicer.vtkMRMLLabelMapVolumeNode() + volume_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") data = helpers.numberArrayToLabelArray(data) for value in nullValue: data[np.where(data == value)] = 0 else: if nullValue is not None: nullValue = handle_null_values(data, nullValue) - volume_node = slicer.vtkMRMLScalarVolumeNode() - volume_node.SetName(name) + volume_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + + name = name.strip() # remove trailing spaces + volume_node.SetName(slicer.mrmlScene.GenerateUniqueName(name)) volume_node.SetAttribute(SCALAR_VOLUME_TYPE, WELL_PROFILE_TAG) volume_node.SetAttribute(NULL_VALUE_TAG, str(nullValue)) + volume_node.SetAttribute(WELL_NAME_TAG, well_name) + + volume_units = units if units != "" else None + volume_node.SetAttribute(UNITS_TAG, units) volume_node.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value) codedUnit = slicer.vtkCodedEntry() codedUnit.SetCodeValue(units) volume_node.SetVoxelValueUnits(codedUnit) - slicer.mrmlScene.AddNode(volume_node) # updateVolumeFromArray does not support long long type if data.dtype == np.longlong: @@ -759,6 +854,7 @@ def create_depth_curves_table( app_folder, name: str = "curves", null_value=None, + well_name="", ): """[summary] @@ -804,9 +900,26 @@ def create_depth_curves_table( return None, None table_node = dataFrameToTableNode(dataFrame=df) table_node.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value) + table_node.SetAttribute(WELL_NAME_TAG, well_name) + + data_units = units if units != "" else None + # The first column should hold the depth (or time, if supported), and all others should have the same unit + if isinstance(units, (list, np.ndarray)): + data_units = units[1] if units[1] != "" else None + else: + data_units = units if units != "" else None + table_node.SetAttribute(UNITS_TAG, data_units) + table_node.SetName(name) - for collumnTitle, unit in zip(df.columns, units): - table_node.SetColumnUnitLabel(collumnTitle, unit) + + if isinstance(units, (list, np.ndarray)): + for collumnTitle, unit in zip(df.columns, units): + table_node.SetColumnUnitLabel(collumnTitle, unit) + else: # if units is a single string, we have 2 columns: depth and the log data + table_node.SetColumnUnitLabel( + df.columns[0], "mm" + ) # NOTE - We expect that mm is enforced when loading the domain data + table_node.SetColumnUnitLabel(df.columns[1], units) subject_hierarchy = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) volume_item_id = subject_hierarchy.CreateItem(root_id, table_node) @@ -819,14 +932,7 @@ def create_depth_curves_table( def create_depth_curves_table_from_image( - curves: dict, - root_folder, - folder, - frame, - units, - app_folder, - name: str = "curves", - null_value=None, + curves: dict, root_folder, folder, frame, units, app_folder, name: str = "curves", null_value=None, well_name="" ): """[summary] @@ -865,6 +971,10 @@ def create_depth_curves_table_from_image( return None, None table_node = dataFrameToTableNode(dataFrame=df) table_node.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value) + table_node.SetAttribute(WELL_NAME_TAG, well_name) + + data_units = units if units != "" else None + table_node.SetAttribute(UNITS_TAG, data_units) table_node.SetAttribute(TableType.name(), TableType.HISTOGRAM_IN_DEPTH.value) table_node.SetAttribute(TableDataTypeAttribute.name(), TableDataTypeAttribute.IMAGE_2D.value) diff --git a/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py b/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py index 697ab71..893e3ac 100644 --- a/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py +++ b/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py @@ -50,10 +50,11 @@ def buildFileInputWidget(self): formLayout = qt.QFormLayout(ioFileInputFrame) self.ioFileInputLineEdit = ctk.ctkPathLineEdit() self.ioFileInputLineEdit.setObjectName("File Input") - self.ioFileInputLineEdit.filters = ctk.ctkPathLineEdit.Files + self.ioFileInputLineEdit.filters = ctk.ctkPathLineEdit.Files | ctk.ctkPathLineEdit.Readable self.ioFileInputLineEdit.settingKey = "ioFileInputMicrotom" # Needs to be initialized blank to allow loading the file metadata self.ioFileInputLineEdit.setCurrentPath("") + self.ioFileInputLineEdit.findChild("QLineEdit").enabled = False self.nullValuesListText = qt.QLineEdit() self.nullValuesListText.text = str(DEFAULT_NULL_VALUE)[1:-1] @@ -70,29 +71,35 @@ def buildFileInputWidget(self): formLayout.addRow(wellDiameterLabel, self.wellDiameter) def onPathChanged(filepath): - self.dataLoader = get_loader(filepath) - nullvalues = set(self.nullValuesListText.text.split(",")) - nullvalues = set(map(float, nullvalues)) - nullvalues.union(self.dataLoader.null_value) - self.nullValuesListText.text = str(nullvalues)[1:-1] - - try: - well_name, metadata = self.dataLoader.load_metadata() - self.wellNameInput.text = well_name - if well_name is None: + with ProgressBarProc() as progressBar: + progressBar.nextStep(5, "Starting to load metadata...") + self.dataLoader = get_loader(filepath) + progressBar.nextStep(80, "Setting context variables...") + nullvalues = set(self.nullValuesListText.text.split(",")) + nullvalues = set(map(float, nullvalues)) + nullvalues.union(self.dataLoader.null_value) + self.nullValuesListText.text = str(nullvalues)[1:-1] + + try: + progressBar.nextStep(90, "Showing metadata...") + well_name, metadata = self.dataLoader.load_metadata() + self.wellNameInput.text = well_name + if well_name is None: + return + self.tableView.setDatabase(metadata) + except LoaderError as e: + slicer.util.infoDisplay(str(e)) + self.ioFileInputLineEdit.setCurrentPath("") + progressBar.nextStep(100, "Error loading metadata.") return - self.tableView.set_database(metadata) - except LoaderError as e: - slicer.util.infoDisplay(str(e)) - self.ioFileInputLineEdit.setCurrentPath("") - return + + progressBar.nextStep(100, "Finished loading metadata.") wellDiameterApplies = True try: wellDiameterApplies = self.dataLoader.loaded_as_image except AttributeError: # no loaded_as_image pass - wellDiameterApplies &= not isinstance(self.dataLoader, LASLoader) self.wellDiameter.setVisible(wellDiameterApplies) wellDiameterLabel.setVisible(wellDiameterApplies) if not wellDiameterApplies: @@ -122,6 +129,7 @@ def progressCallback(progressLabel, progressValue): try: curves = self.dataLoader.load_data(self.ioFileInputLineEdit.currentPath, mnemonic_and_files) + helpers.save_path(self.ioFileInputLineEdit) nullvalues = set(self.nullValuesListText.text.split(",")) @@ -129,23 +137,15 @@ def progressCallback(progressLabel, progressValue): well_diameter = float(self.wellDiameter.text) * 25.4 # inches to mm well_name = self.wellNameInput.text - if isinstance(self.dataLoader, (DLISLoader, CSVLoader)): - itemIDs = self.dataLoader.load_volumes( - curves, - stepCallback=progressCallback, - appFolder=self.appFolder, - nullValue=nullvalues, - well_diameter_mm=well_diameter, - well_name=well_name, - ) - else: - itemIDs = self.dataLoader.load_volumes( - curves, - stepCallback=progressCallback, - appFolder=self.appFolder, - nullValue=nullvalues, - well_diameter_mm=well_diameter, - ) + + itemIDs = self.dataLoader.load_volumes( + curves, + stepCallback=progressCallback, + appFolder=self.appFolder, + nullValue=nullvalues, + well_diameter_mm=well_diameter, + well_name=well_name, + ) self.loadClicked(itemIDs) progressBar.nextStep(100, f"Finished Loading Files.") diff --git a/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py b/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py index b86252a..2b93b01 100644 --- a/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py +++ b/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py @@ -1,6 +1,7 @@ import qt from .DLISImportLogic import ChannelMetadata +from .DLISImportLogic import checkMnemonicChanged from ltrace.slicer import ui @@ -26,7 +27,10 @@ def __init__(self, parent=None): self.selected_rows = 0 - self.columns = ["Load", "Id", "Name", "Unit", "Frame", "Logical file", "LabelMap", "As Table"] + self.columns = ["Load", "Id", "Name", "Unit", "Frame", "Logical file", "LabelMap", "As Table", "Stack"] + + self.stacked_rows_visibility = [] # Boolean for row visibility according to Stack criteria + self.ID_COLUMN = self.columns.index("Id") self.NAME_COLUMN = self.columns.index("Name") @@ -52,6 +56,7 @@ def __init__(self, parent=None): self.tableWidget.verticalHeader().setVisible(False) self.tableWidget.setColumnCount(len(self.columns)) self.tableWidget.setHorizontalHeaderLabels(self.columns) + self.tableWidget.setColumnHidden(self.columns.index("Stack"), True) self.tableWidget.horizontalHeader().setStyleSheet( "QHeaderView::section {padding-left: 10px; padding-right: 10px;}" ) @@ -85,20 +90,81 @@ def __init__(self, parent=None): def clearFilter(self): self.filterLineEdit.text = "" - def set_database(self, db): + def updateStackedVisibility(self): + self.stacked_rows_visibility.clear() + stacking = False + stacked_count = 0 + last_curve_mnemonic = "" + count = 0 + + db_to_columns_offset = 1 # because self.columns has "Load" as the first column, absent in self.db + + for i in range(len(self.db)): + entry = self.db[i] + + # Just the first of a group of rows marked as stacked is shown in the table, + # as such rows will be merged into volumes (see LASLoader class) + last_curve_mnemonic, is_same_mnemonic = checkMnemonicChanged( + entry[self.ID_COLUMN - db_to_columns_offset], last_curve_mnemonic + ) + + stacked = entry[self.columns.index("Stack") - db_to_columns_offset] + + self.stacked_rows_visibility.append(True) + self.tableWidget.setRowHidden(i, False) + visibility = True + if not is_same_mnemonic: + stacked_count = int(stacked) + else: + if stacked: + stacked_count += 1 + if stacked_count > 1: + self.tableWidget.setRowHidden(i, True) + visibility = False + else: + stacked_count = 0 + + self.stacked_rows_visibility[i] = visibility + + def setDatabase(self, db): + def manage_checkboxes(i): + if len(checkBoxes_load) == i: + checkBoxes_load.append(buildCheckBoxCell(False)) + self.tableWidget.setCellWidget(i, 0, checkBoxes_load[i]) + if len(checkBoxes_islabelmap) == i: + checkBoxes_islabelmap.append(buildCheckBoxCell(False)) + self.tableWidget.setCellWidget(i, self.columns.index("LabelMap"), checkBoxes_islabelmap[i]) + if len(checkBoxes_stack) == i: + checkBoxes_stack.append(buildCheckBoxCell(False)) + self.tableWidget.setCellWidget(i, self.columns.index("Stack"), checkBoxes_stack[i]) + return checkBoxes_load[i], checkBoxes_islabelmap[i], checkBoxes_stack[i] + + checkBoxes_load = [] + checkBoxes_islabelmap = [] + checkBoxes_stack = [] + self.db = db current_filter = self.filterLineEdit.text self.filterLineEdit.text = "" self.tableWidget.clearContents() self.tableWidget.setRowCount(len(self.db)) + db_to_columns_offset = 1 # because self.columns has "Load" as the first column, absent in self.db + for i in range(len(self.db)): - loadItem = buildCheckBoxCell(False) + loadItem, checkbox_islabelmap, checkbox_stack = manage_checkboxes(i) loadItem.findChild(qt.QCheckBox).stateChanged.connect(self._on_loaded_checkbox_changed) + self.tableWidget.setCellWidget(i, 0, loadItem) entry = self.db[i] + + # Even though this column is hidden, its checkbox state will be used + col_stack = self.columns.index("Stack") + widget = self.tableWidget.cellWidget(i, col_stack) + widget.layout().itemAt(0).widget().setChecked(entry[col_stack - db_to_columns_offset]) + for j in range(1, len(self.columns)): - if isinstance(entry[j - 1], bool): # j - 1 because entry is 1 shorter than self.columns + if isinstance(entry[j - db_to_columns_offset], bool): item = buildCheckBoxCell(entry[j - 1]) if j == self.columns.index("LabelMap"): item.findChild(qt.QCheckBox).stateChanged.connect(self._on_checkbox_label_clicked) @@ -106,12 +172,14 @@ def set_database(self, db): item.findChild(qt.QCheckBox).stateChanged.connect(self._on_checkbox_table_clicked) self.tableWidget.setCellWidget(i, j, item) else: - item = qt.QTableWidgetItem(entry[j - 1]) + item = qt.QTableWidgetItem(entry[j - db_to_columns_offset]) flags = ~qt.Qt.ItemIsEditable original_flags = item.flags() item.setFlags(qt.Qt.ItemFlag(original_flags and flags)) self.tableWidget.setItem(i, j, item) + self.updateStackedVisibility() + self.tableWidget.sortByColumn(0, qt.Qt.AscendingOrder) self.tableWidget.resizeColumnsToContents() self.filterLineEdit.text = current_filter @@ -128,7 +196,9 @@ def _on_filter_changed(self, current_text): ) entry = entry.casefold() - should_hide = not (any(word in entry for word in current_text) or current_text == []) + should_hide = ( + not (any(word in entry for word in current_text) or current_text == []) + ) or self.stacked_rows_visibility[i] == False self.tableWidget.setRowHidden(i, should_hide) def _on_header_clicked(self, column_clicked): @@ -239,7 +309,7 @@ def rows_to_load(self): widget = self.tableWidget.cellWidget(i, load_column) try: item = widget.layout().itemAt(0).widget() - if item.isChecked(): + if item.isChecked() and not self.tableWidget.isRowHidden(i): rows.append(i) except AttributeError: # no "Labelmap" pass @@ -261,6 +331,8 @@ def _on_load_clicked(self): row_args.append(item.isChecked()) if self.columns[i] == "As Table": row_args.append(item.isChecked()) + if self.columns[i] == "Stack": + row_args.append(item.isChecked()) channel = ChannelMetadata(*row_args) selected_mnemonic_and_files.append(channel) diff --git a/src/modules/DepthRangeSegmenterEffect/DepthRangeSegmenterEffectLib/SegmentEditorEffect.py b/src/modules/DepthRangeSegmenterEffect/DepthRangeSegmenterEffectLib/SegmentEditorEffect.py index 6b59a1b..2948696 100644 --- a/src/modules/DepthRangeSegmenterEffect/DepthRangeSegmenterEffectLib/SegmentEditorEffect.py +++ b/src/modules/DepthRangeSegmenterEffect/DepthRangeSegmenterEffectLib/SegmentEditorEffect.py @@ -162,6 +162,9 @@ def __init__(self, scriptedEffect, sliceWidget): def _getColor(self): color = [0, 0.6, 0.2] + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return color # Get color of edited segment segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if not segmentationNode: @@ -228,6 +231,9 @@ def createPolyData(self): return polyData def addPoint(self, ras): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return # Add a world space point to the current outline # Store active slice when first point is added @@ -294,6 +300,9 @@ def positionActors(self): self.sliceWidget.sliceView().scheduleRender() def apply(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + masterNode = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() diff --git a/src/modules/ExpandSegmentsEffect/ExpandSegmentsEffectLib/SegmentEditorEffect.py b/src/modules/ExpandSegmentsEffect/ExpandSegmentsEffectLib/SegmentEditorEffect.py index 6627bcc..9e38309 100644 --- a/src/modules/ExpandSegmentsEffect/ExpandSegmentsEffectLib/SegmentEditorEffect.py +++ b/src/modules/ExpandSegmentsEffect/ExpandSegmentsEffectLib/SegmentEditorEffect.py @@ -92,6 +92,9 @@ def createCursor(self, widget): return slicer.util.mainWindow().cursor def onApply(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + self.scriptedEffect.saveStateForUndo() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() @@ -141,6 +144,10 @@ def onApply(self): self.applyFinishedCallback() def onApplyFull(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + def getLazySegmentation(parentName: str) -> Union[None, slicer.vtkMRMLNode]: segmentationNode = None lazyNodes = slicer.util.getNodesByClass("vtkMRMLTextNode") diff --git a/src/modules/Export/Export.py b/src/modules/Export/Export.py index 0df425c..50b617a 100644 --- a/src/modules/Export/Export.py +++ b/src/modules/Export/Export.py @@ -3,6 +3,7 @@ from collections import namedtuple from pathlib import Path from threading import Lock +import re import ctk import cv2 @@ -642,8 +643,11 @@ def __exportTableAsCsv(self, node, rootPath, nodePath): csvRows.append(",".join(str(s) for s in csvRow)) path = rootPath / nodePath + adequatedNodeName = re.sub( + r"[\\/*.<>ç?:]", "_", node.GetName() + ) # avoiding characters not suitable for file name path.mkdir(parents=True, exist_ok=True) - with open(str(path / Path(node.GetName() + ".csv")), mode="w", newline="") as csvFile: + with open(str(path / Path(adequatedNodeName + ".csv")), mode="w", newline="") as csvFile: writer = csv.writer(csvFile, delimiter="\n") writer.writerow(csvRows) diff --git a/src/modules/GeologEnv/GeologEnv.py b/src/modules/GeologEnv/GeologEnv.py new file mode 100644 index 0000000..21136bf --- /dev/null +++ b/src/modules/GeologEnv/GeologEnv.py @@ -0,0 +1,85 @@ +import os +import qt +from pathlib import Path + +import GeologLib +from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic + +try: + from Test.GeologEnvTest import GeologEnvTest +except ImportError: + GeologEnvTest = None # tests not deployed to final version or closed source + + +class GeologEnv(LTracePlugin): + SETTING_KEY = "GeologEnv" + MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__))) + + def __init__(self, parent): + LTracePlugin.__init__(self, parent) + self.parent.title = "Geolog Integration" + self.parent.categories = ["LTrace Tools"] + self.parent.contributors = ["LTrace Geophysics Team"] + self.parent.helpText = GeologEnv.help() + + @classmethod + def readme_path(cls): + return str(cls.MODULE_DIR / "README.md") + + +class GeologEnvWidget(LTracePluginWidget): + + GEOLOG_DIRECTORY = "geologDirectory" + PYTHON_DIRECTORY = "pythonDirectory" + PROJECTS_DIRECTORY = "projectsDirectory" + + def __init__(self, parent): + LTracePluginWidget.__init__(self, parent) + + def setup(self): + LTracePluginWidget.setup(self) + self.mainTab = qt.QTabWidget() + + geologImportWidget = GeologLib.GeologImportWidget() + self.mainTab.addTab(geologImportWidget, "Import from Geolog") + + geologExportWidget = GeologLib.GeologExportWidget() + self.mainTab.addTab(geologExportWidget, "Export to Geolog") + + geologImportWidget.signalGeologDataFetched.connect( + lambda instalationPath, projectsPath, project, geologData: self._updateConnectWidget( + instalationPath, projectsPath, project, geologData, geologExportWidget + ) + ) + geologExportWidget.signalGeologDataFetched.connect( + lambda instalationPath, projectsPath, project, geologData: self._updateConnectWidget( + instalationPath, projectsPath, project, geologData, geologImportWidget + ) + ) + + self.layout.addWidget(self.mainTab) + self._loadSettings(geologImportWidget, geologExportWidget) + + def _updateConnectWidget(self, geologPath, projectsPath, project, geologData, widget): + widget.geologConnectWidget.populateFields(geologPath, projectsPath, project) + widget.onGeologDataFetched(geologData) + self._saveSettings(geologPath, projectsPath) + + def _saveSettings(self, geologPath, projectsPath): + GeologEnv.set_setting(self.GEOLOG_DIRECTORY, geologPath) + GeologEnv.set_setting(self.PROJECTS_DIRECTORY, projectsPath) + + def _loadSettings(self, importWidget, exportWidget): + geologPath = GeologEnv.get_setting(self.GEOLOG_DIRECTORY, default=str(Path.home())) + projectsPath = GeologEnv.get_setting(self.PROJECTS_DIRECTORY, default=str(Path.home())) + + importWidget.geologConnectWidget.populateFields(geologPath, projectsPath) + exportWidget.geologConnectWidget.populateFields(geologPath, projectsPath) + + +class GeologEnvLogic(LTracePluginLogic): + def __init__(self): + LTracePluginLogic.__init__(self) + + def apply(self, data): + pass diff --git a/src/modules/GeologEnv/GeologLib/GeologConnectWidget.py b/src/modules/GeologEnv/GeologLib/GeologConnectWidget.py new file mode 100644 index 0000000..8a2721e --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/GeologConnectWidget.py @@ -0,0 +1,265 @@ +import json +import logging +import shutil +import subprocess +import sys +from pathlib import Path + +import qt, ctk, slicer +from Customizer import Customizer +from ltrace.slicer import ui +from ltrace.utils.ProgressBarProc import ProgressBarProc + + +GEOLOG_SCRIPT_ERRORS = { + -1: "Unknown error occurred. Please check if connection parameters are correct", + 1: "Could not initialize project", + 2: "Script execution failed when trying to open WELL", + 3: "Script execution failed when trying to open SET", + 4: "Script execution failed when trying to open LOG", + 5: "Failed when trying to write attributes file when reading project data", + 6: "Failed when trying to read attributes file when importing data", + 7: "Failed when trying to write log data binary files", + 8: "Failed when trying to read log data binary files", + 9: "Failed to read logs attributes during export.", + 11: "Failed to read geolog data when connecting", + 12: "Failed to find json attributes connection file. File must not have been created properly", + 13: "Failed to find temorary folder with connecting data.", + 21: "Selected items are not valid due to different spacing or types", + 22: "Selected 3D items are not exportable.", + 23: "Some items were skipped from export due to incompatible type.", + 24: "A SET was found with the given name and overwrite was not allowed", + 31: "Finished with errors. Some logs binary files could not be read and were skipped during export", + 32: "Finished with errors. Some logs could not be opened and were skiped during export", + 33: "Finished with errors. Could not add attributes to log and were skipped during export", + 34: "Script execution failed due to all logs being skipped.", + 35: "Failed to write log data into geolog. Script execution was aborted", + 41: "Finished with error. Some wells could not be opened and were skipped.", + 42: "Finished with error. Some sets could not be opened and were skipped.", + 43: "Finished with error. Some logs could not be opened and were skipped.", +} + + +class GeologConnectWidget(qt.QWidget): + NO_PROJECT = "No project found in this directory" + signalGeologData = qt.Signal(object) + signalScriptError = qt.Signal(int, str) + + def __init__(self, parent=None, prefix=""): + """ + Handles connection to Geolog, using path and host to find and retrieve Geologs project well data. + + Args: + prefix (str): prefix text to be added to widgets object names to facilitate searching. + """ + + super().__init__(parent) + + self.geologData = None + self.setup(prefix) + + self.loadClicked = lambda *a: None + + def setup(self, prefix): + if sys.platform == "win32": + geologUsualPath = "'C:/Program Files/Paradigm/Geolog22.0' for Windows systems" + projectUsualPath = "'C:/programData/Paradigm/projects' for Windows systems (the programData folder may be hidden by default)" + else: + geologUsualPath = "'/home/USER/Paradigm/Geolog22.0' for Linux systems" + projectUsualPath = "'/home/USER/Paradigm/projects/' for Linux systems" + + self.geologInstalation = ctk.ctkDirectoryButton() + self.geologInstalation.caption = "Geolog instalation folder" + self.geologInstalation.objectName = f"{prefix} Geolog Directory Browser" + self.geologInstalation.directoryChanged.connect(self.checkSearchButtonState) + self.geologInstalation.setToolTip( + f"Select the path to Geolog folder. Default installation is usually in {geologUsualPath}. (Example path)" + ) + + self.geologProjectsFolder = ctk.ctkDirectoryButton() + self.geologProjectsFolder.caption = "Geolog project parent folder" + self.geologProjectsFolder.directoryChanged.connect(self.onProjectPathSelected) + self.geologProjectsFolder.objectName = f"{prefix} Geolog Projects Directory Browser" + self.geologProjectsFolder.setToolTip( + f"Select the path to Geolog project directory. Usually is {projectUsualPath}" + ) + + self.projectComboBox = qt.QComboBox() + self.projectComboBox.currentIndexChanged.connect(self.checkSearchButtonState) + self.projectComboBox.objectName = f"{prefix} Geolog Project Selector ComboBox" + self.projectComboBox.setToolTip("Projects available in Geolog projects folder") + + self.refreshButton = qt.QPushButton() + self.refreshButton.clicked.connect(self.onProjectPathSelected) + self.refreshButton.setIcon(qt.QIcon(str(Customizer.RESET_ICON_PATH))) + self.refreshButton.setFixedWidth(30) + self.refreshButton.setToolTip("Refresh the directory to check for newly created projects") + + projectLayout = qt.QHBoxLayout() + projectLayout.addWidget(self.projectComboBox) + projectLayout.addWidget(self.refreshButton) + projectLayout.setContentsMargins(0, 0, 0, 0) + + projectSelectorWidget = qt.QWidget() + projectSelectorWidget.setLayout(projectLayout) + + self.searchButton = ui.ApplyButton( + onClick=self.searchButtonClicked, + tooltip="Search for well, sets and logs in the given project", + enabled=True, + ) + self.searchButton.setText("Read project") + self.searchButton.enabled = False + self.searchButton.objectName = f"{prefix} Geolog Connect Button" + + self.status = qt.QLabel("Status: Idle") + self.status.setAlignment(qt.Qt.AlignRight | qt.Qt.AlignVCenter) + self.status.setWordWrap(True) + + layout = qt.QFormLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addRow("Geolog directory:", self.geologInstalation) + layout.addRow("Projects directory:", self.geologProjectsFolder) + layout.addRow("Project:", projectSelectorWidget) + layout.addRow(self.searchButton) + layout.addRow(self.status) + layout.addRow("", None) + + self.setLayout(layout) + self.onProjectPathSelected() + + self.logic = GeologConnectLogic() + + def populateFields(self, instalationPath, projectsPath, project=""): + self.geologInstalation.directory = instalationPath + self.geologProjectsFolder.directory = projectsPath + if project: + self.projectComboBox.setCurrentText(project) + + def updateStatus(self, code=0, message="Finished!"): + statusMessage = "Status: " + if code: + statusMessage = f"{statusMessage}Error code: {code} - " + self.status.setStyleSheet("font-weight: bold; color: red") + else: + self.status.setStyleSheet("font-weight: bold; color: green") + self.status.setText(f"{statusMessage}{message}") + + def getEnvs(self): + geologPath = Path(f"{self.geologInstalation.directory}/bin/geolog").as_posix() + scriptPath = Path(__file__).parent.as_posix() + + return geologPath, scriptPath + + def searchButtonClicked(self): + with ProgressBarProc() as progressBar: + if not self.checkEnvViability(): + self.updateStatus(-1, GEOLOG_SCRIPT_ERRORS[-1]) + return + + progressBar.setMessage("Getting data") + geologPath, scriptPath = self.getEnvs() + + try: + geologData = self.logic.getGeologData(geologPath, scriptPath, self.projectComboBox.currentText) + except GeologScriptError as e: + self.updateStatus(e.errorCode, e.errorMessage) + else: + self.signalGeologData.emit(geologData) + self.updateStatus() + + def onProjectPathSelected(self): + self.projectComboBox.clear() + projectList = [f.name for f in Path(self.geologProjectsFolder.directory).iterdir() if f.is_dir()] + if projectList: + self.projectComboBox.enabled = True + for project in projectList: + self.projectComboBox.addItem(project) + else: + self.projectComboBox.enabled = False + self.projectComboBox.addItem(self.NO_PROJECT) + self.checkSearchButtonState() + + def checkEnvViability(self): + geologPath, scriptPath = self.getEnvs() + + if sys.platform == "win32": + if not Path(geologPath + ".exe").is_file(): + return False + else: + if not Path(geologPath).is_file(): + return False + + return True + + def checkSearchButtonState(self): + envIsValid = self.checkEnvViability() + if envIsValid and self.projectComboBox.currentText != self.NO_PROJECT: + self.searchButton.enabled = True + else: + self.searchButton.enabled = False + + +class GeologConnectLogic(object): + def getGeologData(self, geologPath, scriptPath, projectName): + scriptPath = f"{scriptPath}/scriptConnect.py" + temporaryPath = Path(slicer.util.tempDirectory()) + + args = [geologPath, "mod_python", scriptPath, "--project", projectName, "--tempPath", temporaryPath] + + try: + self._runProcess(args) + except subprocess.CalledProcessError as e: + if GEOLOG_SCRIPT_ERRORS.get(e.returncode, -1) == -1: + raise GeologScriptError(-1, GEOLOG_SCRIPT_ERRORS[-1]) + raise GeologScriptError(e.returncode, GEOLOG_SCRIPT_ERRORS[e.returncode]) from e + else: + return self._readGeologOutput(temporaryPath) + + def _runProcess(self, args): + proc = slicer.util.launchConsoleProcess(args) + slicer.util.logProcessOutput(proc) + logging.info(f"Connect process still running: {proc.poll()}") + + def _readGeologOutput(self, temporaryPath): + code = 0 + if Path(temporaryPath).is_dir(): + file = f"{temporaryPath.as_posix()}/output.json" + if Path(file).is_file(): + try: + with open(file, "r") as f: + geologData = json.load(f) + except FileNotFoundError: + # Failed when trying to read connectScript output json file. + code = 11 + else: + # Could not find connectScript output json file + code = 12 + else: + # Could not find temporary directory + code = 13 + + self._cleanUp(temporaryPath) + + if code: + raise GeologScriptError(code, GEOLOG_SCRIPT_ERRORS.get(code, GEOLOG_SCRIPT_ERRORS[-1])) + + return geologData + + def _cleanUp(self, temporaryPath): + if temporaryPath.is_dir(): + shutil.rmtree(temporaryPath, ignore_errors=True) + + +class GeologScriptError(BaseException): + """Except raised for errors during GEOLOG script execution + + Args: + errorCode (int): Error code + errorMessage (str): Error message + """ + + def __init__(self, errorCode=-1, errorMessage="Unknown error occurred"): + self.errorCode = errorCode + self.errorMessage = errorMessage + super().__init__(self.errorMessage) diff --git a/src/modules/GeologEnv/GeologLib/GeologExportWidget.py b/src/modules/GeologEnv/GeologLib/GeologExportWidget.py new file mode 100644 index 0000000..50d478d --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/GeologExportWidget.py @@ -0,0 +1,427 @@ +import json +import logging +import re +import shutil +import subprocess +from pathlib import Path + +import numpy as np +import qt, ctk, slicer +import vtk +from ltrace.slicer import ui +from ltrace.slicer.helpers import highlight_error, remove_highlight +from ltrace.slicer.node_attributes import TableType +from ltrace.slicer.widget.help_button import HelpButton +from ltrace.utils.ProgressBarProc import ProgressBarProc +from .GeologConnectWidget import GeologConnectWidget, GEOLOG_SCRIPT_ERRORS, GeologScriptError + +NEW_WELL = "New Well" +NEW_SET = "New Set" +EXPORTABLE_TYPES = ( + slicer.vtkMRMLScalarVolumeNode, + slicer.vtkMRMLTableNode, + slicer.vtkMRMLLabelMapVolumeNode, +) + + +class GeologExportWidget(qt.QWidget): + signalGeologDataFetched = qt.Signal(str, str, str, object) + + def __init__(self, parent=None): + """ + Widget to let the user select data nodes and export them to Geolog + """ + super().__init__(parent) + + self.geologData = None + self.setup() + + def setup(self): + + self.geologConnectWidget = GeologConnectWidget(prefix="export") + self.geologConnectWidget.signalGeologData.connect(lambda geologData: self.onGeologDataFetched(geologData, True)) + + section = ctk.ctkCollapsibleButton() + section.text = "Export to Geolog" + section.collapsed = False + + layoutWell = qt.QHBoxLayout() + layoutWell.setContentsMargins(0, 0, 0, 0) + + self.exportWellComboBox = qt.QComboBox() + self.exportWellComboBox.setMinimumWidth(200) + self.exportWellComboBox.currentIndexChanged.connect(self.onExportwellChanged) + self.exportWellComboBox.objectName = "Export Well Selector ComboBox" + + self.wellName = qt.QLineEdit() + self.wellName.placeholderText = "Type a well Name" + self.wellName.objectName = "Export Well Name LineEdit" + + layoutWell.addWidget(self.exportWellComboBox) + layoutWell.addWidget(self.wellName) + + self.exportWellWidget = qt.QWidget() + self.exportWellWidget.setLayout(layoutWell) + + layoutSet = qt.QHBoxLayout() + layoutSet.setContentsMargins(0, 0, 0, 0) + + self.newSetName = qt.QLineEdit() + self.newSetName.placeholderText = "Type a set Name" + self.newSetName.objectName = "Export Set Name LineEdit" + + self.overwriteCheckBox = qt.QCheckBox("Overwrite SET") + self.overwriteCheckBox.objectName = "Export Allow Set Overwrite CheckBox" + + overwriteHelp = HelpButton( + "If there is a set with the same name as the export set, checking this option will overwrite the set and its data in Geolog. If unchecked the export will terminate if a set with the same name is present." + ) + + layoutSet.addWidget(self.newSetName) + layoutSet.addWidget(self.overwriteCheckBox) + layoutSet.addWidget(overwriteHelp) + + self.exportSetWidget = qt.QWidget() + self.exportSetWidget.setLayout(layoutSet) + + self.subjectHierarchyTreeView = slicer.qMRMLSubjectHierarchyTreeView() + self.subjectHierarchyTreeView.setMRMLScene(slicer.app.mrmlScene()) + self.subjectHierarchyTreeView.header().setVisible(False) + self.subjectHierarchyTreeView.nodeTypes = [exportable.__name__ for exportable in EXPORTABLE_TYPES] + for i in range(2, 6): + self.subjectHierarchyTreeView.hideColumn(i) + self.subjectHierarchyTreeView.setEditMenuActionVisible(False) + self.subjectHierarchyTreeView.contextMenuEnabled = False + self.subjectHierarchyTreeView.setFocusPolicy(qt.Qt.NoFocus) + self.subjectHierarchyTreeView.objectName = "Export Node Selector TreeView" + self.subjectHierarchyTreeView.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + + self.exportButton = ui.ApplyButton( + onClick=self.onExportButtonClick, tooltip="Export the selected nodes to Geolog", enabled=True + ) + self.exportButton.objectName = "Export Button" + self.status = qt.QLabel("Status: Idle") + self.status.setAlignment(qt.Qt.AlignRight | qt.Qt.AlignVCenter) + self.status.setWordWrap(True) + + sectionLayout = qt.QFormLayout(section) + sectionLayout.addRow(self.geologConnectWidget) + sectionLayout.addRow("Choose a well:", self.exportWellWidget) + sectionLayout.addRow("Choose a set:", self.exportSetWidget) + sectionLayout.addRow(self.subjectHierarchyTreeView) + sectionLayout.addRow(self.exportButton) + sectionLayout.addRow(self.status) + + self.setLayout(sectionLayout) + + self.updateWellComboBox() + + self.logic = GeologExportLogic() + + def checkRunState(self): + isValid = True + if not self.newSetName.text: + highlight_error(self.newSetName) + isValid = False + else: + remove_highlight(self.newSetName) + + if not self.wellName.text: + highlight_error(self.wellName) + isValid = False + else: + remove_highlight(self.wellName) + + if not self._getNodesToExport(): + highlight_error(self.subjectHierarchyTreeView) + isValid = False + else: + remove_highlight(self.subjectHierarchyTreeView) + + if not self.geologConnectWidget.checkEnvViability(): + isValid = False + self.updateStatus(-1, GEOLOG_SCRIPT_ERRORS[-1]) + + return isValid + + def updateStatus(self, code=0, message="Finished!"): + statusMessage = "Status: " + if code: + statusMessage = f"{statusMessage}Error code: {code} - " + self.status.setStyleSheet("font-weight: bold; color: red") + else: + self.status.setStyleSheet("font-weight: bold; color: green") + self.status.setText(f"{statusMessage}{message}") + + def onGeologDataFetched(self, geologData, emitToEnv=False): + self.geologData = geologData + self.updateWellComboBox() + if emitToEnv: + self.signalGeologDataFetched.emit( + self.geologConnectWidget.geologInstalation.directory, + self.geologConnectWidget.geologProjectsFolder.directory, + self.geologConnectWidget.projectComboBox.currentText, + geologData, + ) + + def onExportwellChanged(self): + if self.exportWellComboBox.currentText == NEW_WELL: + self.wellName.enabled = True + self.wellName.text = "" + else: + self.wellName.enabled = False + self.wellName.text = self.exportWellComboBox.currentText + + def updateWellComboBox(self): + self.exportWellComboBox.clear() + self.exportWellComboBox.addItem(NEW_WELL) + if self.geologData: + for well in self.geologData: + self.exportWellComboBox.addItem(well) + + def _checkNodesViability(self, nodes): + spacing = set([]) + nodeType = set([]) + has3D = [] + for node in nodes: + nodeType.add(node.__class__.__name__) + if isinstance(node, slicer.vtkMRMLScalarVolumeNode): + spacing.add(node.GetSpacing()[2]) + dimensions = node.GetImageData().GetDimensions() + if all(axis > 1 for axis in dimensions): + has3D.append(node.GetName()) + + if len(spacing) > 1 or (len(nodeType) > 1 and slicer.vtkMRMLLabelMapVolumeNode in nodeType): + self.updateStatus(21, GEOLOG_SCRIPT_ERRORS[21]) + return False + elif has3D: + self.updateStatus(22, f"{GEOLOG_SCRIPT_ERRORS[22]} 3D nodes: {', '.join(has3D)}") + return False + + return True + + def _getNodesToExport(self): + subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + items = vtk.vtkIdList() + self.subjectHierarchyTreeView.currentItems(items) + nodes = [] + for id in range(items.GetNumberOfIds()): + itemId = items.GetId(id) + dataNode = subjectHierarchyNode.GetItemDataNode(itemId) + if dataNode is not None and type(dataNode) in EXPORTABLE_TYPES: + nodes.append(dataNode) + + nodesAreValid = self._checkNodesViability(nodes) + + return nodes if nodesAreValid else [] + + def onExportButtonClick(self): + if not self.checkRunState(): + return + + exportNodes = self._getNodesToExport() + if not exportNodes: + return + + geologPath, scriptPath = self.geologConnectWidget.getEnvs() + well = self.wellName.text + logicalFile = self.newSetName.text + + overwrite = self.overwriteCheckBox.isChecked() + + with ProgressBarProc() as progressBar: + try: + self.logic.exportGeologData( + self.geologConnectWidget.projectComboBox.currentText, + well, + logicalFile, + exportNodes, + overwrite, + geologPath, + scriptPath, + ) + except GeologScriptError as e: + self.updateStatus(e.errorCode, e.errorMessage) + else: + self.updateStatus() + + +class GeologExportLogic(object): + def exportGeologData(self, project, well, logicalFile, exportNodes, overwrite, geologPath, scriptPath): + scriptPath = f"{scriptPath}/scriptExport.py" + temporaryPath = Path(slicer.util.tempDirectory()) + + if isinstance(exportNodes[0], slicer.vtkMRMLScalarVolumeNode): + wrongTypeNodes = self._createVolumeExportFiles(exportNodes, temporaryPath) + else: + wrongTypeNodes = self._createTableExportFiles(exportNodes, temporaryPath) + + args = [ + geologPath, + "mod_python", + scriptPath, + "--project", + project, + "--well", + well, + "--set", + logicalFile, + "--overwrite", + str(1 if overwrite else 0), + "--tempPath", + temporaryPath, + ] + + try: + self._runProcess(args) + except subprocess.CalledProcessError as e: + if GEOLOG_SCRIPT_ERRORS.get(e.returncode, -1) == -1: + raise GeologScriptError(-1, GEOLOG_SCRIPT_ERRORS[-1]) + raise GeologScriptError(e.returncode, GEOLOG_SCRIPT_ERRORS[e.returncode]) from e + else: + if wrongTypeNodes: + GeologScriptError(23, f"{GEOLOG_SCRIPT_ERRORS[23]} Nodes: {', '.join(wrongTypeNodes)}") + finally: + self._cleanUp(temporaryPath) + + def _cleanUp(self, temporaryPath): + if temporaryPath.is_dir(): + shutil.rmtree(temporaryPath, ignore_errors=True) + + def _runProcess(self, args): + proc = slicer.util.launchConsoleProcess(args) + slicer.util.logProcessOutput(proc) + logging.info(f"Export process still running: {proc.poll()}") + + def _createBinaryFile(self, node, logName, temporaryPath): + if isinstance(node, slicer.vtkMRMLTableNode): + df = slicer.util.dataframeFromTable(node) + arr = np.array(df)[:, 1:] + if node.GetAttribute(TableType.name()) == TableType.POROSITY_PER_REALIZATION.value: + arr = np.reshape(np.mean(arr, axis=1), (-1, 1)) + else: + arr = slicer.util.arrayFromVolume(node)[:, 0, :] + nullValue = node.GetAttribute("NullValue") + if nullValue: + arr[arr == float(nullValue)] = -9999 + arr[np.isnan(arr)] = -9999 + + file = f"{temporaryPath}/{logName}.npy" + if not Path(file).is_file(): + np.save(file, arr) + + def _getNameAndUnit(self, name): + pattern = r"\[(.*?)\]" + outputString = re.split(pattern, name) + logName = outputString[0].strip() + + unit = None + if len(outputString) > 1: + unit = outputString[1] + + return logName, unit + + def _createTableExportFiles(self, exportNodes, temporaryPath): + logsAttributes = {} + wrongTypeNodes = [] + for node in exportNodes: + if not isinstance(node, slicer.vtkMRMLTableNode): + wrongTypeNodes.append(node.GetName()) + continue + + logName, unit = self._getNameAndUnit(node.GetName()) + + self._createBinaryFile(node, logName, temporaryPath) + logsAttributes[logName] = self.getNodeAttributes(node, unit) + + self._saveAttributesJson(logsAttributes, "table", temporaryPath) + + return wrongTypeNodes + + def _createVolumeExportFiles(self, exportNodes, temporaryPath): + logsAttributes = {} + wrongTypeNodes = [] + for node in exportNodes: + if not isinstance(node, slicer.vtkMRMLScalarVolumeNode): + wrongTypeNodes.append(node.GetName()) + continue + + browserNode = slicer.modules.sequences.logic().GetFirstBrowserNodeForProxyNode(node) + if browserNode: + sequenceNode = browserNode.GetSequenceNode(node) + + logName, unit = self._getNameAndUnit(node.GetName()) + + if browserNode: + for image in range(sequenceNode.GetNumberOfDataNodes()): + nthNode = sequenceNode.GetNthDataNode(image) + nthName = f"{logName}_r{image}" + self._createBinaryFile(nthNode, nthName, temporaryPath) + logsAttributes[nthName] = self.getNodeAttributes(nthNode, unit) + else: + self._createBinaryFile(node, logName, temporaryPath) + logsAttributes[logName] = self.getNodeAttributes(node, unit) + + self._saveAttributesJson(logsAttributes, "volume", temporaryPath) + + return wrongTypeNodes + + def _saveAttributesJson(self, logsAttributes, nodeType, temporaryPath): + depthTop = np.inf + depthBottom = 0 + depthIncrement = 0 + for key in logsAttributes.keys(): + if logsAttributes[key]["top"] < depthTop: + depthTop = logsAttributes[key]["top"] + if logsAttributes[key]["bottom"] > depthBottom: + depthBottom = logsAttributes[key]["bottom"] + depthIncrement = logsAttributes[key]["spacing"] + + depthLog = { + "name": "DEPTH", + "top": depthTop, + "bottom": depthBottom, + "spacing": depthIncrement, + "type": "DOUBLE", + "unit": "METRES", + } + + exportJson = {"nodeType": nodeType, "reference": depthLog, "logs": logsAttributes} + + with open(f"{temporaryPath}/attributes.json", "w", encoding="utf-8") as f: + json.dump(exportJson, f, ensure_ascii=False, indent=4) + + def getNodeAttributes(self, node, unit=None): + if isinstance(node, slicer.vtkMRMLTableNode): + df = slicer.util.dataframeFromTable(node) + arr = np.array(df) + + unitConversion = 1000 + if node.GetAttribute(TableType.name()) == TableType.POROSITY_PER_REALIZATION.value: + unitConversion = 1 + + top = arr[0, 0] / unitConversion + bottom = arr[-1, 0] / unitConversion + height = arr.shape[0] + verticalSpacing = (bottom - top) / (height - 1) + + else: + origins = -np.round(node.GetOrigin(), 5) / 1000 + spacings = np.round(node.GetSpacing(), 5) / 1000 + dimensions = np.array(node.GetImageData().GetDimensions()) + top = origins[2] + bottom = origins[2] + dimensions[2] * spacings[2] + height = dimensions[2] + verticalSpacing = spacings[2] + + nodeAttributes = { + "top": top, + "bottom": bottom, + "height": int(height), + "spacing": float(verticalSpacing), + "type": "REAL" if isinstance(node, slicer.vtkMRMLLabelMapVolumeNode) else "DOUBLE", + } + + nodeAttributes["unit"] = unit + + return nodeAttributes diff --git a/src/modules/GeologEnv/GeologLib/GeologImportWidget.py b/src/modules/GeologEnv/GeologLib/GeologImportWidget.py new file mode 100644 index 0000000..f56425e --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/GeologImportWidget.py @@ -0,0 +1,356 @@ +import logging +import shutil +import subprocess +from pathlib import Path + +import qt, slicer +import numpy as np +import pandas as pd +import DLISImportLib +from ltrace.image.optimized_transforms import DEFAULT_NULL_VALUE, handle_null_values +from ltrace.slicer import ui +from ltrace.slicer.helpers import highlight_error, remove_highlight +from ltrace.slicer.node_attributes import ( + ImageLogDataSelectable, + TableType, + TableDataOrientation, + TableDataTypeAttribute, +) +from ltrace.slicer_utils import dataFrameToTableNode +from ltrace.utils.ProgressBarProc import ProgressBarProc +from .GeologConnectWidget import GeologConnectWidget, GEOLOG_SCRIPT_ERRORS, GeologScriptError + + +class GeologImportWidget(qt.QWidget): + NO_WELL = "No Well Found" + + signalGeologDataFetched = qt.Signal(str, str, str, object) + + def __init__(self, parent=None): + """ + Widget that allows the user to visualize the available data in a Geolog project and to import it into Geoslicer + """ + super().__init__(parent) + + self.importedData = None + self.setup() + + def setup(self): + self.geologConnectWidget = GeologConnectWidget(prefix="import") + self.geologConnectWidget.signalGeologData.connect(lambda geologData: self.onGeologDataFetched(geologData, True)) + + self.wellComboBox = qt.QComboBox() + self.wellComboBox.currentIndexChanged.connect(self._updateTable) + self.wellComboBox.objectName = "Import Well Selector ComboBox" + self.wellComboBox.setToolTip("Wells found in the Geolog Project") + + self.wellDiameter = ui.floatParam("") + self.wellDiameter.setObjectName("Well Diameter Input") + self.wellDiameter.objectName = "Import Well Diameter LineEdit" + self.wellDiameter.setToolTip( + "Diameter of the well in inches. This will be used to calculate the horizontal spacing of the imported data" + ) + + self.nullValuesListText = qt.QLineEdit() + self.nullValuesListText.text = str(DEFAULT_NULL_VALUE)[1:-1] + self.nullValuesListText.objectName = "Import Null Values LineEdit" + self.nullValuesListText.setToolTip( + "Values that represent null values. They will be changed to nan values during import." + ) + + self.tableView = DLISImportLib.DLISTableViewer() + self.tableView.setMinimumHeight(500) + self.tableView.loadClicked = self._onLoadClicked + self.tableView.objectName = "dataTableView" + + self.status = qt.QLabel("Status: Idle") + self.status.setAlignment(qt.Qt.AlignRight | qt.Qt.AlignVCenter) + self.status.setWordWrap(True) + + layout = qt.QFormLayout() + layout.addRow(self.geologConnectWidget) + layout.addRow("", None) + layout.addRow("Null values list:", self.nullValuesListText) + layout.addRow("Well diameter (inches):", self.wellDiameter) + layout.addRow("Well name:", self.wellComboBox) + layout.addRow(self.tableView) + layout.addRow(self.status) + + self.setLayout(layout) + + self.logic = GeologImportLogic() + self._updateWellComboBox() + + def updateStatus(self, code=0, message="Finished!"): + statusMessage = "Status: " + if code: + statusMessage = f"{statusMessage}Error code: {code} - " + self.status.setStyleSheet("font-weight: bold; color: red") + else: + self.status.setStyleSheet("font-weight: bold; color: green") + self.status.setText(f"{statusMessage}{message}") + + def checkRunState(self): + if not self.wellDiameter.text: + highlight_error(self.wellDiameter) + return False + else: + remove_highlight(self.wellDiameter) + + if not self.nullValuesListText.text: + highlight_error(self.nullValuesListText) + return False + else: + remove_highlight(self.nullValuesListText) + + return True + + def _onLoadClicked(self, data): + if not self.checkRunState(): + return + + geologPath, scriptPath = self.geologConnectWidget.getEnvs() + well = self.wellComboBox.currentText + setData = {} + loadAsTable = {} + loadAsLabelmap = {} + for log in data: + logicalFile = getattr(log, "logical_file") + mnemonic = getattr(log, "mnemonic") + asTable = getattr(log, "is_table") + asLabelmap = getattr(log, "is_labelmap") + + if logicalFile not in setData: + setData[logicalFile] = [] + loadAsTable[logicalFile] = {} + loadAsLabelmap[logicalFile] = {} + + setData[logicalFile].append(mnemonic) + loadAsTable[logicalFile][mnemonic] = asTable + loadAsLabelmap[logicalFile][mnemonic] = asLabelmap + + with ProgressBarProc() as progressBar: + try: + for logical in setData: + self.logic.importGeologData( + logical, + setData[logical], + self.geologConnectWidget.projectComboBox.currentText, + well, + float(self.wellDiameter.text), + self.nullValuesListText.text, + loadAsTable[logical], + loadAsLabelmap[logical], + geologPath, + scriptPath, + self.importedData, + ) + except GeologScriptError as e: + self.updateStatus(e.errorCode, e.errorMessage) + else: + self.updateStatus() + + def _updateTable(self): + tableData = [] + if self.wellComboBox.currentText != "" and self.wellComboBox.currentText != self.NO_WELL: + sets = self.importedData[self.wellComboBox.currentText] + else: + sets = None + if sets: + for setName, logs in sets.items(): + if logs: + for logName, attributes in logs.items(): + tableData.append( + DLISImportLib.DLISImportLogic.ChannelMetadata( + logName, + attributes["comment"], + attributes["unit"], + attributes["sr"], + setName, + False, + False, + False, + ) + ) + + self.tableView.setDatabase(tableData) + + def onGeologDataFetched(self, geologData, emitToEnv=False): + if geologData: + self.importedData = geologData + else: + self.importedData = None + + self._updateWellComboBox() + if emitToEnv: + self.signalGeologDataFetched.emit( + self.geologConnectWidget.geologInstalation.directory, + self.geologConnectWidget.geologProjectsFolder.directory, + self.geologConnectWidget.projectComboBox.currentText, + geologData, + ) + + def _updateWellComboBox(self): + self.wellComboBox.clear() + if self.importedData: + self.wellComboBox.enabled = True + for well in self.importedData: + self.wellComboBox.addItem(well) + else: + self.wellComboBox.addItem(self.NO_WELL) + self.wellComboBox.enabled = False + + +class GeologImportLogic(object): + def importGeologData( + self, + logicalFile, + logList, + project, + wellName, + wellDiameter, + nullValueString, + loadAsTable, + loadAsLabelmap, + geologPath, + scriptPath, + importedData, + ): + scriptPath = f"{scriptPath}/scriptImport.py" + + temporaryPath = Path(slicer.util.tempDirectory()) + + args = [ + geologPath, + "mod_python", + scriptPath, + "--project", + project, + "--well", + wellName, + "--set", + logicalFile, + "--log", + *logList, + "--tempPath", + temporaryPath, + ] + + try: + self._runProcess(args) + except subprocess.CalledProcessError as e: + if GEOLOG_SCRIPT_ERRORS.get(e.returncode, -1) == -1: + GeologScriptError(-1, GEOLOG_SCRIPT_ERRORS[-1]) + raise GeologScriptError(e.returncode, GEOLOG_SCRIPT_ERRORS[e.returncode]) from e + else: + self._readImportedData( + wellName, + wellDiameter, + nullValueString, + logicalFile, + logList, + loadAsTable, + loadAsLabelmap, + temporaryPath, + importedData, + ) + finally: + self._cleanUp(temporaryPath) + + def _runProcess(self, args): + proc = slicer.util.launchConsoleProcess(args) + slicer.util.logProcessOutput(proc) + logging.info(f"Import process still running: {proc.poll()}") + + def _readImportedData( + self, + well: str, + wellDiameter: float, + nullValueString: str, + logicalFile: str, + logList: list, + loadAsTable: object, + loadAsLabelmap: object, + temporaryPath: str, + importedData: object, + ): + for log in logList: + data = np.load(f"{temporaryPath}/{log}.npy") + if importedData[well][logicalFile][log]["repeat"] > 1 and not loadAsTable[log]: + self._loadAsVolume( + well, + wellDiameter, + nullValueString, + data, + importedData[well][logicalFile][log], + log, + loadAsLabelmap[log], + ) + else: + self._loadAsTable(well, nullValueString, data, importedData[well][logicalFile][log], log) + + def _loadAsVolume( + self, wellName, wellDiameterInches, nullValueString, logData, logAttributes, logName, loadAsLabelmap + ): + wellDiameter = wellDiameterInches * 25.4 + horizontalSpacing = (wellDiameter * np.pi) / logData.shape[1] + + top = float(logAttributes["top"]) + bottom = float(logAttributes["bottom"]) + imageOrigin = [wellDiameter * np.pi / 2, 0, -top * 1000] + spacing = [horizontalSpacing, 0.48, (((bottom - top) * 1000) / (logData.shape[0] - 1))] + + nodeType = "vtkMRMLLabelMapVolumeNode" if loadAsLabelmap else "vtkMRMLScalarVolumeNode" + + unitString = f" [{logAttributes['unit']}]" if logAttributes["unit"] else "" + nodeName = f"{wellName}_{logName}{unitString}" + + volume = slicer.mrmlScene.AddNewNodeByClass(nodeType, nodeName) + + nullValues = np.array(nullValueString.split(",")).astype(np.float32) + if not loadAsLabelmap: + nullValue = handle_null_values(logData, nullValues) + else: + for value in nullValues: + logData[np.where(logData == value)] = 0 + nullValue = 0 + + values3D = np.zeros((logData.shape[0], 1, logData.shape[1])) + values3D[:, 0, :] = logData + slicer.util.updateVolumeFromArray(volume, values3D) + + volume.SetAttribute("NullValue", str(nullValue)) + volume.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value) + volume.SetIJKToRASDirections(-1, 0, 0, 0, -1, 0, 0, 0, -1) + volume.SetOrigin(imageOrigin) + volume.SetSpacing(spacing) + + def _loadAsTable(self, wellName, nullValueString, logData, logAttributes, logName): + top = float(logAttributes["top"]) * 1000 + bottom = float(logAttributes["bottom"]) * 1000 + nullValues = np.array(nullValueString.split(",")).astype(np.float32) + logValues = np.where(np.isin(logData, nullValues), np.nan, logData) + + df = pd.DataFrame(logValues.astype(float)) + + depthCurve = np.linspace(top, bottom, num=logValues.shape[0]) + df.insert(0, "DEPTH", depthCurve.astype(float)) + + tableNode = dataFrameToTableNode(dataFrame=df) + + unitString = f" [{logAttributes['unit']}]" if logAttributes["unit"] else "" + nodeName = f"{wellName}_{logName}{unitString}" + tableNode.SetName(nodeName) + + tableNode.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value) + + if logValues.shape[1] > 1: + tableNode.SetAttribute(TableType.name(), TableType.HISTOGRAM_IN_DEPTH.value) + tableNode.SetAttribute(TableDataTypeAttribute.name(), TableDataTypeAttribute.IMAGE_2D.value) + tableNode.SetAttribute(TableDataOrientation.name(), TableDataOrientation.ROW.value) + tableNode.SetUseFirstColumnAsRowHeader(True) + tableNode.SetUseColumnNameAsColumnHeader(True) + + def _cleanUp(self, temporaryPath): + if temporaryPath.is_dir(): + shutil.rmtree(temporaryPath, ignore_errors=True) diff --git a/src/modules/GeologEnv/GeologLib/__init__.py b/src/modules/GeologEnv/GeologLib/__init__.py new file mode 100644 index 0000000..fcf818c --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/__init__.py @@ -0,0 +1,3 @@ +from .GeologImportWidget import * +from .GeologExportWidget import * +from .GeologConnectWidget import * diff --git a/src/modules/GeologEnv/GeologLib/scriptConnect.py b/src/modules/GeologEnv/GeologLib/scriptConnect.py new file mode 100644 index 0000000..6211539 --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/scriptConnect.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python-real + +import argparse +import json +import sys + +import pygg + + +def __init_project(project): + if not pygg.init(project): + sys.exit(1) + else: + print(f"Initialized project (RO): {project}") + + +def __open(type, status, name): + if not pygg.open(type, status, name): + print(f"Could not open {type} ({status}): {name}...") + raise ValueError + + +def main(project, temporary_folder): + return_code = 0 + + __init_project(project) + + output = {} + + while True: + result, well_name = pygg.getc(pygg.NEXT_WELL, pygg.STATUS_OLD) + if not result: + break + try: + __open(pygg.WELL, pygg.STATUS_OLD, well_name) + except ValueError: + return_code = 41 + continue + + well_sets = {} + + while True: + result, set_name = pygg.getc(pygg.NEXT_SET, pygg.STATUS_OLD) + if not result: + break + try: + __open(pygg.SET, pygg.STATUS_OLD, set_name) + except Exception: + return_code = 42 + continue + + set_logs = {} + + while True: + result, log_name = pygg.getc(pygg.NEXT_LOG, pygg.STATUS_OLD) + if not result: + break + try: + __open(pygg.LOG, pygg.STATUS_OLD, log_name) + except Exception: + return_code = 43 + continue + + log_attributes = {} + + result, units = pygg.getc(pygg.LOG_UNITS, log_name) + result, comment = pygg.getc(pygg.LOG_COMMENT, log_name) + result, sr = pygg.getc(pygg.LOG_SR, log_name) + result, repeat = pygg.getn(pygg.LOG_REPEAT, log_name) + result, dir = pygg.getc(pygg.LOG_LOGGED_DIRECTION, log_name) + result, dimension = pygg.getc(pygg.LOG_DIMENSION, log_name) + result, top = pygg.getc(pygg.LOG_TOP, log_name) + result, bottom = pygg.getc(pygg.LOG_BOTTOM, log_name) + result, frames = pygg.getc("*FRAMES", log_name) + + log_attributes["comment"] = comment + log_attributes["unit"] = units + log_attributes["sr"] = sr + log_attributes["repeat"] = repeat + log_attributes["dir"] = dir + log_attributes["dimension"] = dimension + log_attributes["frames"] = frames + log_attributes["top"] = top + log_attributes["bottom"] = bottom + + set_logs[log_name] = log_attributes + + pygg.close(pygg.SET, pygg.STATUS_OLD, set_name) + + well_sets[set_name] = set_logs + + output[well_name] = well_sets + + pygg.close(pygg.WELL, pygg.STATUS_OLD, well_name) + + pygg.term() + + try: + with open(f"{temporary_folder}/output.json", "w", encoding="utf-8") as f: + json.dump(output, f, ensure_ascii=False, indent=4) + except: + sys.exit(5) + + return return_code + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--project", + help="Geolog Project", + ) + parser.add_argument( + "--tempPath", + help="Geolog set", + required=True, + ) + args = parser.parse_args() + + code = main(args.project, args.tempPath) + + sys.exit(code) diff --git a/src/modules/GeologEnv/GeologLib/scriptExport.py b/src/modules/GeologEnv/GeologLib/scriptExport.py new file mode 100644 index 0000000..0a6dc8d --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/scriptExport.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python-real + +import argparse +import json +import sys + +import numpy as np +import pygg + + +class GeologOpenException(BaseException): + pass + + +def __init_project(project): + if not pygg.init(project): + sys.exit(1) + else: + print("Initialized project (RO): %s" % project) + + +def __open(type, status, name): + if not pygg.open(type, status, name): + raise GeologOpenException + else: + print(f"Opened {type} ({status}): {name}") + + +def main(project, well, logical_file, overwrite, export_folder): + return_code = 0 + + __init_project(project) + + try: + with open(f"{export_folder}/attributes.json", "r") as f: + export_data = json.load(f) + except FileNotFoundError: + sys.exit(9) + + well_status = pygg.STATUS_NEW + try: + __open(pygg.WELL, well_status, well) + except GeologOpenException: + sys.exit(2) + + try: + __open(pygg.SET, pygg.STATUS_OLD, logical_file) + pygg.close(pygg.SET, pygg.STATUS_OLD, logical_file) + + if overwrite: + raise (GeologOpenException) + + raise (ValueError) + + except ValueError: + sys.exit(24) + + except GeologOpenException: + pass + + try: + set_status = pygg.STATUS_NEW + __open(pygg.SET, set_status, logical_file) + except GeologOpenException: + sys.exit(3) + + reference = export_data["reference"] + depth_top = reference["top"] + depth_bottom = reference["bottom"] + depth_increment = reference["spacing"] + + try: + log_status = pygg.STATUS_NEW + __open(pygg.LOG, pygg.STATUS_NEW, reference["name"]) + except GeologOpenException: + sys.exit(4) + + log_values_dict = {} + log_attributes = export_data["logs"] + for log_name in log_attributes.keys(): + try: + log_values_dict[log_name] = np.load(f"{export_folder}/{log_name}.npy") + except FileNotFoundError: + return_code = 31 + log_attributes.pop(log_name, None) + print(f"couldnt not get {log_name} files. skipping log...") + continue + + # REFERENCE attrs + pygg.putc(pygg.LOG_UNITS, reference["name"], reference["name"]) + pygg.putc(pygg.LOG_DIMENSION, reference["name"], "1") + pygg.putc(pygg.LOG_COMMENT, reference["name"], "Imported from GEOSLICER") + + removed_logs = [] + + for log_name in log_values_dict.keys(): + try: + log_type = log_attributes[log_name]["type"] + __open(pygg.LOG + f"{log_type}*{log_values_dict[log_name].shape[1]}", log_status, log_name) + except GeologOpenException: + return_code = 32 + removed_logs.append(log_name) + print(f"could not open log {log_name}. skipping log...") + continue + + # LOG attrs + try: + unit = log_attributes[log_name]["unit"] + dimension = log_values_dict[log_name].shape[1] + repeat = log_values_dict[log_name].shape[1] + top = float(log_attributes[log_name]["top"]) + bottom = float(log_attributes[log_name]["bottom"]) + + if unit: + pygg.putc(pygg.LOG_UNITS, log_name, unit) + pygg.putc(pygg.LOG_DIMENSION, log_name, str(dimension)) + pygg.putn(pygg.LOG_REPEAT, log_name, repeat) + pygg.putn(pygg.LOG_TOP, log_name, top) + pygg.putn(pygg.LOG_BOTTOM, log_name, bottom) + pygg.putc(pygg.LOG_COMMENT, log_name, "Imported from GEOSLICER") + except ValueError: + return_code = 33 + removed_logs.append(log_name) + continue + + if removed_logs: + for to_be_removed in removed_logs: + log_attributes.pop(to_be_removed, None) + log_values_dict.pop(to_be_removed, None) + + if not log_attributes or not log_values_dict: + sys.exit(34) + + try: + current_depth = depth_top + while current_depth < depth_bottom: + pygg.putn(pygg.LOG_VALUE, reference["name"], float(current_depth)) + for log_name in log_attributes.keys(): + log_top = log_attributes[log_name]["top"] + log_bottom = log_attributes[log_name]["bottom"] + if current_depth >= log_top: + if current_depth <= log_bottom: + index = int((current_depth - log_top) / depth_increment) + pygg.putn(pygg.LOG_VALUES, log_name, log_values_dict[log_name][index].tolist()) + else: + width = len(log_values_dict[log_name][0].tolist()) + pygg.putn(pygg.LOG_VALUES, log_name, [np.nan for z in range(width)]) + + pygg.write() + current_depth += depth_increment + except RuntimeError: + sys.exit(35) + + for log_name in log_attributes.keys(): + pygg.close(pygg.LOG, log_status, log_name) + + pygg.close(pygg.LOG, pygg.STATUS_NEW, reference["name"]) + pygg.close(pygg.SET, set_status, logical_file) + pygg.close(pygg.WELL, well_status, well) + pygg.term() + + sys.exit(return_code) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--project", + help="Geolog Project", + required=True, + ) + parser.add_argument( + "--well", + help="Geolog well", + required=True, + ) + parser.add_argument( + "--set", + help="Geolog set", + required=True, + ) + parser.add_argument( + "--overwrite", + help="Allow overwriting set with given name", + required=True, + ) + parser.add_argument( + "--tempPath", + help="Geolog set", + required=True, + ) + args = parser.parse_args() + + overwrite = True if int(args.overwrite) else False + + main(args.project, args.well, args.set, overwrite, args.tempPath) diff --git a/src/modules/GeologEnv/GeologLib/scriptImport.py b/src/modules/GeologEnv/GeologLib/scriptImport.py new file mode 100644 index 0000000..12aa613 --- /dev/null +++ b/src/modules/GeologEnv/GeologLib/scriptImport.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python-real + +import argparse +import sys + +import numpy as np +import pygg + + +OPEN_ERROR_CODES = {pygg.WELL: 2, pygg.SET: 3, pygg.LOG: 4} + + +def __init_project(project): + if not pygg.init(project): + sys.exit(1) + else: + print(f"Initialized project (RO): {project}") + + +def __open(type, status, name): + if not pygg.open(type, status, name): + sys.exit(OPEN_ERROR_CODES[type]) + else: + print(f"Opened {type} ({status}): {name}") + + +def main(project, well, sets, logs, temporary_folder): + __init_project(project) + __open(pygg.WELL, pygg.STATUS_OLD, well) + __open(pygg.SET, pygg.STATUS_OLD, sets) + + for log in logs: + __open(pygg.LOG, pygg.STATUS_OLD, log) + result, repeat = pygg.getn(pygg.LOG_REPEAT, log) + log_values = [] + while pygg.read(): + result, log_val = pygg.getn(pygg.LOG_VALUES, log, repeat=int(repeat)) + if not result: + break + log_values.append(log_val) + + np.save(f"{temporary_folder}/{log}.npy", log_values) + + pygg.close(pygg.LOG, pygg.STATUS_OLD, log) + + pygg.close(pygg.SET, pygg.STATUS_OLD, sets) + pygg.close(pygg.WELL, pygg.STATUS_OLD, well) + pygg.term() + + sys.exit(0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--project", + help="Geolog Project", + required=True, + ) + parser.add_argument( + "--well", + help="Geolog well", + required=True, + ) + parser.add_argument( + "--set", + help="Geolog set", + required=True, + ) + parser.add_argument("--log", help="Geolog log", required=True, nargs="+") + parser.add_argument( + "--tempPath", + help="Geolog set", + required=True, + ) + args = parser.parse_args() + + main(args.project, args.well, args.set, args.log, args.tempPath) diff --git a/src/modules/GeologEnv/README.md b/src/modules/GeologEnv/README.md new file mode 100644 index 0000000..5656916 --- /dev/null +++ b/src/modules/GeologEnv/README.md @@ -0,0 +1,30 @@ +# Geolog Env + +This module provides _GeoSlicer_ an interface for connecting to _Geolog_. Allowing to import/export data from/to _Geolog_. This module requires _Geolog_ Python 3.8 as it contains some libraries and tools used during execution. + +## Inputs +### Connection +1. __Geolog directory__: Select the directory of _Geolog_ installation. The usual directory path is "C:/Program Files/Paradigm/Geolog22.0" for Windows and "/home/USER/Paradigm/" in Linux. Version 22.0 was used during development. + +2. __Projects folder__: Select the directory contaning _Geolog_ projects that are going to be accessed. The usual directory path is "C:/programData/Paradigm/projects" in Windows and "/home/USER/Paradigm/projects/" in Linux. + +3. __Project__: _Geolog_ projects available in the chosen project directory. + +### Importing from _GEOLOG_ + +1. __Null values list__: List of values that are considered to be null. they will be changed to nan after the data is imported into _Geoslicer_. + +2. __Well diameter (inches)__: Diameter of the well which the data is being imported from. This is used for setting the vetical spacing of the imagelog. + +3. __Well Name__: Name of the wells avaliable to choose from after connecting to the project. + +4. __Log Table__: Table containing the logs available in the well. + + +### Exporting to _GEOLOG_ + +1. __Choose a well__: The dropdown list allows for easly selecting a target well to export. Choosing "New Well" option allows for user to create new well, although setting the name of an existing well will not overwrite it, but act as if a well was choosen. + +2. __Choose a set__: Name of the set that will be created during export. At the moment we cannot add the data to an existing set, only creating a new set. Choosing a name of a existing set will overwrite it if the checkbox is selected. If not, the export will be terminated. + +3. __Data selection__: Widget containig the _Geoslicer_ tree view to select which volumes are to be exported. Due to how we write the data into _Geoslicer_, the volumes must have the same vertical spacing to avoid gaps in _Geolog_ \ No newline at end of file diff --git a/src/modules/GeologEnv/Resources/Icons/GeologEnv.svg b/src/modules/GeologEnv/Resources/Icons/GeologEnv.svg new file mode 100644 index 0000000..546dbe0 --- /dev/null +++ b/src/modules/GeologEnv/Resources/Icons/GeologEnv.svg @@ -0,0 +1,51 @@ + + icon_geolog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/HistogramSegmenter/HistogramSegmenter.py b/src/modules/HistogramSegmenter/HistogramSegmenter.py index 5957aca..5369cb8 100644 --- a/src/modules/HistogramSegmenter/HistogramSegmenter.py +++ b/src/modules/HistogramSegmenter/HistogramSegmenter.py @@ -313,9 +313,6 @@ def buildControls(self): return buttonsLayout - def cleanup(self): - pass - def updateLoadingInfo(self, text=None): if text is None: self.loadingInfoLabel.setText("") @@ -374,7 +371,7 @@ def onSelectOutput(self, outputVolumeNode): setDimensionFrom(getNodeById(self.inputSelector.currentItem()), outputVolumeNode) self.applyButton.enabled = True except AttributeError as ae: - logging.error(f"{repr(ae)}") + print(repr(ae)) def onReloadHistogram(self): inputVolumeNode = getNodeById(self.inputSelector.currentItem()) diff --git a/src/modules/ImageLogData/ImageLogData.py b/src/modules/ImageLogData/ImageLogData.py index 410e4c5..b0accac 100644 --- a/src/modules/ImageLogData/ImageLogData.py +++ b/src/modules/ImageLogData/ImageLogData.py @@ -185,6 +185,13 @@ def currentItemChanged(self, itemID_bogus): def exit(self): self.logic.exit() + def cleanup(self): + super().cleanup() + self.logic.layoutViewOpened.disconnect() + self.logic.layoutViewClosed.disconnect() + self.logic.viewsRefreshed.disconnect() + self.logic.addViewClicked.disconnect() + class ViewDataEncoder(json.JSONEncoder): def default(self, obj): @@ -1133,7 +1140,11 @@ def reloadImageLogSegmenterEffect(self): if not hasattr(self, "imageLogSegmenterWidget"): return - segmentEditorWidget = self.imageLogSegmenterWidget.self().segmentEditorWidget + try: + segmentEditorWidget = self.imageLogSegmenterWidget.self().segmentEditorWidget + except ValueError: + # imageLogSegmenterWidget was deleted + return activeEffect = segmentEditorWidget.activeEffect() segmentEditorWidget.setActiveEffectByName("None") segmentEditorWidget.setActiveEffect(activeEffect) @@ -1309,26 +1320,69 @@ def segmentationNodeOrSourceVolumeNodeChanged(self, segmentationNode=None, sourc # Preparing basic views when initializing a segmentation, if no views are present prepareBasicViews = False - if len(self.imageLogViewList) == 0 and segmentationNode is not None and sourceVolumeNode is not None: + if segmentationNode is None or sourceVolumeNode is None: + return + + if len(self.imageLogViewList) == 0: prepareBasicViews = True - for i in range(3): - self.imageLogViewList.append(ImageLogView(sourceVolumeNode, segmentationNode)) + + if not prepareBasicViews: + msg_box = qt.QMessageBox(slicer.util.mainWindow()) + msg_box.setIcon(qt.QMessageBox.Question) + msg_box.setStandardButtons(qt.QMessageBox.Yes | qt.QMessageBox.No) + msg_box.setDefaultButton(qt.QMessageBox.Yes) + + print(segmentationNode.GetName(), sourceVolumeNode.GetName()) + msg_box.text = ( + "Would you like to edit the segmentation in a 3-view layout? This will reset any current views." + ) + msg_box.setWindowTitle("Reset layout") + result = msg_box.exec() + prepareBasicViews = result == qt.QMessageBox.Yes # Three basic views, one with primary node and segmentation, one with segmentation only and one with segmentation proportions if prepareBasicViews: + self.imageLogViewList.clear() + for i in range(3): + self.imageLogViewList.append(ImageLogView(sourceVolumeNode, segmentationNode)) self.__refreshViews("segmentationNodeOrSourceVolumeNodeChanged") + slicer.app.processEvents(1000) identifier = 1 viewControllerWidget = self.viewControllerWidgets[identifier] showHidePrimaryNodeButton = viewControllerWidget.findChild( qt.QPushButton, "showHidePrimaryNodeButton" + str(identifier) ) showHidePrimaryNodeButton.click() + slicer.app.processEvents(1000) identifier = 2 - viewControllerWidget = self.viewControllerWidgets[identifier] - showHideProportionsNodeButton = viewControllerWidget.findChild( - qt.QPushButton, "showHideProportionsNodeButton" + str(identifier) - ) - showHideProportionsNodeButton.click() + # Hack - click 3 times + for i in range(3): + viewControllerWidget = self.viewControllerWidgets[identifier] + showHideProportionsNodeButton = viewControllerWidget.findChild( + qt.QPushButton, "showHideProportionsNodeButton" + str(identifier) + ) + showHideProportionsNodeButton.click() + slicer.app.processEvents(1000) + + def addInpaintView(self, segmentationNode1=None, segmentationNode2=None, sourceVolumeNode=None): + self.configureSliceViewsAllowedSegmentationNodes() + + if len(self.imageLogViewList) != 2: + for id in range(len(self.imageLogViewList) - 1, -1, -1): + self.removeView(id) + + self.imageLogViewList.append(ImageLogView(sourceVolumeNode, segmentationNode1)) + self.imageLogViewList.append(ImageLogView(sourceVolumeNode, segmentationNode2)) + else: + self.imageLogViewList[0] = ImageLogView(sourceVolumeNode, segmentationNode1) + self.imageLogViewList[1] = ImageLogView(sourceVolumeNode, segmentationNode2) + + self.__refreshViews("addInpaintView") + + viewControllerWidget = self.viewControllerWidgets[1] + + showHidePrimaryNodeButton = viewControllerWidget.findChild(qt.QPushButton, "showHidePrimaryNodeButton" + str(1)) + showHidePrimaryNodeButton.click() def getViewName(self, identifier): return self.imageLogViewList[identifier].viewData.VIEW_NAME_PREFIX + str(identifier) @@ -1605,7 +1659,7 @@ def changeOpacitySegmentationNode(self, identifier, value): except AttributeError as e: # In case other controllers haven't been initialized yet logging.debug(e) pass - if segmentationNode != None: + if segmentationNode: if type(segmentationNode) is slicer.vtkMRMLSegmentationNode: segmentationDisplayNode = segmentationNode.GetDisplayNode() segmentationDisplayNode.SetOpacity(value) @@ -1683,7 +1737,7 @@ def onNodeAboutToBeRemoved(self, identifier, node): """ refresh = False self.nodeAboutToBeRemoved = True - if len(self.imageLogViewList) > 0: + if len(self.imageLogViewList) > 0 and identifier < len(self.imageLogViewList): viewData = self.imageLogViewList[identifier].viewData if node is self.getNodeById(viewData.primaryNodeId): self.imageLogViewList[identifier] = ImageLogView(None) diff --git a/src/modules/ImageLogData/ImageLogDataLib/view/image_log_view.py b/src/modules/ImageLogData/ImageLogDataLib/view/image_log_view.py index 7b93b18..1a03d43 100644 --- a/src/modules/ImageLogData/ImageLogDataLib/view/image_log_view.py +++ b/src/modules/ImageLogData/ImageLogDataLib/view/image_log_view.py @@ -10,6 +10,7 @@ from ltrace.slicer.node_attributes import TableType from ltrace.slicer_utils import tableNodeToDict from ltrace.utils.CorrelatedLabelMapVolume import ProportionLabelMapVolume +from ltrace.slicer.helpers import triggerNodeModified import vtk @@ -20,29 +21,23 @@ def __init__(self): def getProportionsLabelMapNode(self, segmentationNode): if segmentationNode is None: return None - proportionsNode = None - # Try to get from subjectHierarchyNode - subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - parentId = subjectHierarchyNode.GetItemParent(subjectHierarchyNode.GetItemByDataNode(segmentationNode)) - vtk_list = vtk.vtkIdList() - subjectHierarchyNode.GetItemChildren(parentId, vtk_list) - for i in range(vtk_list.GetNumberOfIds()): - childItemID = vtk_list.GetId(i) - childName = subjectHierarchyNode.GetItemName(childItemID) - if "_Proportions" in childName: - proportionsNode = subjectHierarchyNode.GetItemDataNode(childItemID) - break + # Try to get from the dictionary + proportionsNode = self.getNodeById(self.proportionsNodesIds.get(segmentationNode.GetID(), None)) # Build a new node if it is not found if proportionsNode is None: proportionLabelMapVolumeName = segmentationNode.GetName() + "_Proportions" plmv = ProportionLabelMapVolume(segmentationNode, proportionLabelMapVolumeName) proportionsNode = plmv.labelMapVolumeNode - proportionsNode.HideFromEditorsOn() - proportionsNode.SetAttribute("ShowInFilteredNodeComboBox", "False") - subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - itemParent = subjectHierarchyNode.GetItemParent(subjectHierarchyNode.GetItemByDataNode(segmentationNode)) - subjectHierarchyNode.SetItemParent(subjectHierarchyNode.GetItemByDataNode(proportionsNode), itemParent) - self.proportionsNodesIds[segmentationNode.GetID()] = proportionsNode.GetID() + if proportionsNode is not None: + proportionsNode.HideFromEditorsOn() + proportionsNode.SetAttribute("ShowInFilteredNodeComboBox", "False") + triggerNodeModified(proportionsNode) # Trigger node modification to apply the hide from editors + subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + itemParent = subjectHierarchyNode.GetItemParent( + subjectHierarchyNode.GetItemByDataNode(segmentationNode) + ) + subjectHierarchyNode.SetItemParent(subjectHierarchyNode.GetItemByDataNode(proportionsNode), itemParent) + self.proportionsNodesIds[segmentationNode.GetID()] = proportionsNode.GetID() return proportionsNode def getNodeById(self, nodeId): @@ -87,14 +82,12 @@ def set_new_segmentation_node(self, segmentationNode): self.viewData.segmentationNodeId = segmentationNode.GetID() if type(segmentationNode) is slicer.vtkMRMLSegmentationNode: if segmentationNode.GetSegmentation().GetNumberOfSegments() <= 10: - self.viewData.proportionsNodeId = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode( - segmentationNode - ).GetID() + proportionNode = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode(segmentationNode) + self.viewData.proportionsNodeId = proportionNode.GetID() if proportionNode is not None else None elif type(segmentationNode) is slicer.vtkMRMLLabelMapVolumeNode: if segmentationNode.GetImageData().GetScalarRange()[1] <= 10: - self.viewData.proportionsNodeId = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode( - segmentationNode - ).GetID() + proportionNode = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode(segmentationNode) + self.viewData.proportionsNodeId = proportionNode.GetID() if proportionNode is not None else None else: self.viewData.segmentationNodeId = None self.viewData.proportionsNodeId = None @@ -157,9 +150,8 @@ def __new_view_data_for_node_and_segment(self, sourceVolumeNode, segmentationNod sliceViewData = SliceViewData() sliceViewData.primaryNodeId = sourceVolumeNode.GetID() sliceViewData.segmentationNodeId = segmentationNode.GetID() - sliceViewData.proportionsNodeId = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode( - segmentationNode - ).GetID() + proportionNode = self.PROPORTION_NODES_LOADER.getProportionsLabelMapNode(segmentationNode) + sliceViewData.proportionsNodeId = proportionNode.GetID() if proportionNode is not None else None return sliceViewData, None def __getParametersForGraphicViewData(self, node): diff --git a/src/modules/ImageLogEnv/ImageLogEnv.py b/src/modules/ImageLogEnv/ImageLogEnv.py index 565c984..5766c80 100644 --- a/src/modules/ImageLogEnv/ImageLogEnv.py +++ b/src/modules/ImageLogEnv/ImageLogEnv.py @@ -14,22 +14,24 @@ from SegmentInspector import SegmentInspector from UnwrapRegistration import UnwrapRegistration from AzimuthShiftTool import AzimuthShiftTool +from ImageLogInpaint import ImageLogInpaint # Checks if closed source code is available try: from ImageLogsLib.Eccentricity import EccentricityWidget except: EccentricityWidget = None -try: - from Test.EccentricityTest import EccentricityTest -except ImportError: - EccentricityTest = None from QualityIndicator import QualityIndicator from SpiralFilter import SpiralFilter from ImageLogExport import ImageLogExport from ImageLogCropVolume import ImageLogCropVolume +try: + from Test.PermeabilityModelingTest import PermeabilityModelingTest +except ImportError: + pass + class ImageLogEnv(LTracePlugin): SETTING_KEY = "ImageLogEnv" @@ -59,6 +61,7 @@ def __init__(self, parent): + UnwrapRegistration.help() + PermeabilityModelingWidget.help() + AzimuthShiftTool.help() + + ImageLogInpaint.help() ) @classmethod @@ -72,24 +75,29 @@ def setup(self): self.logic = ImageLogEnvLogic() self.mainTab = qt.QTabWidget() + self.mainTab.setObjectName("Image Log Main Tab") dataTab = qt.QTabWidget() + dataTab.setObjectName("Image Log Data Tab") processingTab = qt.QTabWidget() self.segmentationTab = qt.QTabWidget() registrationTab = qt.QTabWidget() - self.imageLogDataWidget = slicer.modules.imagelogdata.widgetRepresentation() - self.imageLogCropVolume = slicer.modules.imagelogcropvolume.widgetRepresentation() + self.inpaintTab = qt.QTabWidget() + self.imageLogDataWidget = slicer.modules.imagelogdata.createNewWidgetRepresentation() + self.imageLogCropVolume = slicer.modules.imagelogcropvolume.createNewWidgetRepresentation() self.segment_inspector_env = slicer.modules.segmentinspector.createNewWidgetRepresentation() - self.imageLogSegmenterWidget = slicer.modules.imagelogsegmenter.widgetRepresentation() - self.instanceSegmenterWidget = slicer.modules.imageloginstancesegmenter.widgetRepresentation() - self.instanceSegmenterEditorWidget = slicer.modules.instancesegmentereditor.widgetRepresentation() - self.imageLogUnwrapImportWidget = slicer.modules.imagelogunwrapimport.widgetRepresentation() - self.imageLogExportWidget = slicer.modules.imagelogexport.widgetRepresentation() - self.spiralFiterWidget = slicer.modules.spiralfilter.widgetRepresentation() - self.qualityIndicatorWidget = slicer.modules.qualityindicator.widgetRepresentation() - self.heterogeneityIndexWidget = slicer.modules.heterogeneityindex.widgetRepresentation() - self.unwrapRegistrationWidget = slicer.modules.unwrapregistration.widgetRepresentation() - self.AzimuthShiftToolWidget = slicer.modules.azimuthshifttool.widgetRepresentation() + self.imageLogSegmenterWidget = slicer.modules.imagelogsegmenter.createNewWidgetRepresentation() + self.instanceSegmenterWidget = slicer.modules.imageloginstancesegmenter.createNewWidgetRepresentation() + self.instanceSegmenterEditorWidget = slicer.modules.instancesegmentereditor.createNewWidgetRepresentation() + self.imageLogUnwrapImportWidget = slicer.modules.imagelogunwrapimport.createNewWidgetRepresentation() + self.imageLogExportWidget = slicer.modules.imagelogexport.createNewWidgetRepresentation() + self.spiralFiterWidget = slicer.modules.spiralfilter.createNewWidgetRepresentation() + self.qualityIndicatorWidget = slicer.modules.qualityindicator.createNewWidgetRepresentation() + self.heterogeneityIndexWidget = slicer.modules.heterogeneityindex.createNewWidgetRepresentation() + self.unwrapRegistrationWidget = slicer.modules.unwrapregistration.createNewWidgetRepresentation() + self.AzimuthShiftToolWidget = slicer.modules.azimuthshifttool.createNewWidgetRepresentation() + self.imageLogInpaint = slicer.modules.imageloginpaint.createNewWidgetRepresentation() + self.coreInpaint = slicer.modules.coreinpaint.createNewWidgetRepresentation() self.segment_inspector_env.self().blockVisibilityChanges = True @@ -100,13 +108,15 @@ def setup(self): self.imageLogSegmenterWidget.self().logic.setImageLogDataLogic(imageDataLogic) self.instanceSegmenterEditorWidget.self().logic.setImageLogDataLogic(imageDataLogic) self.imageLogDataWidget.self().logic.setImageLogSegmenterWidget(self.imageLogSegmenterWidget) + self.imageLogInpaint.self().logic.setImageLogDataLogic(imageDataLogic) logImportWidget = DLISImportLib.WellLogImportWidget() + logImportWidget.setObjectName("Well Log Import Widget") logImportWidget.setAppFolder("Well Logs") self.eccentricityWidget = EccentricityWidget() if EccentricityWidget else None - permeabilityModelingWidget = PermeabilityModelingWidget() + self.permeabilityModelingWidget = PermeabilityModelingWidget() cornerButtonsFrame = qt.QFrame() cornerButtonsLayout = qt.QHBoxLayout(cornerButtonsFrame) @@ -160,18 +170,24 @@ def setup(self): # Registration tab registrationTab.addTab(self.unwrapRegistrationWidget, "Unwrap Registration") + # Inpaint tab + self.inpaintTab.addTab(self.imageLogInpaint, "Interactive") + self.inpaintTab.addTab(self.coreInpaint, "Automatic") + self.mainTab.addTab(dataTab, "Data") self.mainTab.addTab(self.imageLogCropVolume, "Crop") self.mainTab.addTab(processingTab, "Processing") self.mainTab.addTab(self.segmentationTab, "Segmentation") self.mainTab.addTab(registrationTab, "Registration") - self.mainTab.addTab(permeabilityModelingWidget, "Modeling") + self.mainTab.addTab(self.permeabilityModelingWidget, "Modeling") + self.mainTab.addTab(self.inpaintTab, "Inpaint") self.lastAccessedWidget = dataTab.widget(0) self.mainTab.tabBarClicked.connect(self.onMainTabClicked) self.segmentationTab.tabBarClicked.connect(self.onSegmentationTabClicked) + self.inpaintTab.tabBarClicked.connect(self.onInpaintTabClicked) self.layout.addWidget(self.mainTab) def onMainTabClicked(self, index): @@ -204,6 +220,14 @@ def onImageLogViewClosed(self): if self.eccentricityWidget: self.eccentricityWidget.logic.process_finished.disconnect(self._on_external_process_finished) + def onInpaintTabClicked(self, index): + if self.lastAccessedWidget != self.inpaintTab.widget( + index + ): # To avoid calling exit by clicking over the active tab + self.lastAccessedWidgetExit() + self.lastAccessedWidget = self.inpaintTab.widget(index) + self.lastAccessedWidgetEnter() + def enter(self) -> None: super().enter() self.logic.setupSliceViewAnnotations() @@ -242,6 +266,14 @@ def enableAddView(self): def fit(self): self.imageLogDataWidget.self().logic.fit() + def cleanup(self): + super().cleanup() + self.imageLogDataWidget.self().cleanup() + self.imageLogSegmenterWidget.self().cleanup() + self.segment_inspector_env.self().cleanup() + self.imageLogInpaint.self().cleanup() + self.eccentricityWidget.logic.process_finished.disconnect() + class ImageLogEnvLogic(LTracePluginLogic): def __init__(self): diff --git a/src/modules/ImageLogEnv/ImageLogsLib/KdsOptimizationTableWidget.py b/src/modules/ImageLogEnv/ImageLogsLib/KdsOptimizationTableWidget.py new file mode 100644 index 0000000..9601c0f --- /dev/null +++ b/src/modules/ImageLogEnv/ImageLogsLib/KdsOptimizationTableWidget.py @@ -0,0 +1,293 @@ +import slicer +import qt +import sys +import pandas as pd +from ltrace.slicer import ui +from ltrace.slicer_utils import tableWidgetToDataFrame + + +class KdsOptimizationItemModel: + def __init__(self) -> None: + self.__kRo = None + self.__kDst = None + self.__initialDepth = None + self.__endDepth = None + + @property + def kRo(self): + return self.__kRo + + @kRo.setter + def kRo(self, value): + self.__kRo = value + + @property + def kDst(self): + return self.__kDst + + @kDst.setter + def kDst(self, value): + self.__kDst = value + + @property + def initialDepth(self): + return self.__initialDepth + + @initialDepth.setter + def initialDepth(self, value): + self.__initialDepth = value + + @property + def endDepth(self): + return self.__endDepth + + @endDepth.setter + def endDepth(self, value): + self.__endDepth = value + + +class AddKdsOptimizationItemDialog(qt.QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.model = KdsOptimizationItemModel() + self.setup() + + def setup(self): + layout = qt.QVBoxLayout() + self.setLayout(layout) + + self.__initialDepthSpinBox = ui.numberParam( + vrange=(0, sys.float_info.max), + value=0.0, + ) + self.__endDepthSpinBox = ui.numberParam(vrange=(0, sys.float_info.max), value=1.0) + self.__kDstSpinBox = ui.numberParam(vrange=(0, sys.float_info.max), value=1.0) + self.__kRoSpinBox = ui.numberParam(vrange=(0, sys.float_info.max), value=1.0, decimals=3) + + self.__initialDepthSpinBox.setKeyboardTracking(False) + self.__endDepthSpinBox.setKeyboardTracking(False) + self.__kDstSpinBox.setKeyboardTracking(False) + self.__kRoSpinBox.setKeyboardTracking(False) + + self.__initialDepthSpinBox.valueChanged.connect(self.__onValuesChanged) + self.__endDepthSpinBox.valueChanged.connect(self.__onValuesChanged) + self.__kDstSpinBox.valueChanged.connect(self.__onValuesChanged) + self.__kRoSpinBox.valueChanged.connect(self.__onValuesChanged) + + formLayout = qt.QFormLayout() + formLayout.addRow("Initial Depth", self.__initialDepthSpinBox) + formLayout.addRow("End Depth", self.__endDepthSpinBox) + formLayout.addRow("K_dst", self.__kDstSpinBox) + formLayout.addRow("K_ro", self.__kRoSpinBox) + + buttonLayout = qt.QHBoxLayout() + okButton = qt.QPushButton("OK") + okButton.clicked.connect(self.__onOkButtonClicked) + + cancelButton = qt.QPushButton("Cancel") + cancelButton.clicked.connect(lambda x: self.reject()) + + buttonLayout.addWidget(okButton) + buttonLayout.addWidget(cancelButton) + + layout.addLayout(formLayout) + layout.addLayout(buttonLayout) + + self.__initialDepthSpinBox.objectName = "Start Depth Spin Box" + self.__endDepthSpinBox.objectName = "Stop Depth Spin Box" + self.__kDstSpinBox.objectName = "Kdst Spin Box" + self.__kRoSpinBox.objectName = "Kro Spin Box" + okButton.objectName = "Add Kds Optimization Item Add Button" + cancelButton.objectName = "Add Kds Optimization Item Cancel Button" + + def __onValuesChanged(self, _): + if self.__endDepthSpinBox.value < self.__initialDepthSpinBox.value: + self.__endDepthSpinBox.value = self.__initialDepthSpinBox.value + 1 + + if self.__initialDepthSpinBox.value > self.__endDepthSpinBox.value: + self.__initialDepthSpinBox.value = self.__endDepthSpinBox.value - 1 + + def __onOkButtonClicked(self, checked): + self.model.kRo = self.__kRoSpinBox.value + self.model.kDst = self.__kDstSpinBox.value + self.model.initialDepth = self.__initialDepthSpinBox.value + self.model.endDepth = self.__endDepthSpinBox.value + + self.accept() + + +class KdsOptimizationWidget(qt.QWidget): + tableUpdated = qt.Signal(object) + HEADER = ["Start Depth [m]", "Stop Depth [m]", "K_dst [m.MD]", "K_ro [frac]"] + + def __init__(self, parent=None): + super().__init__(parent) + self.setup() + + def setup(self): + layout = qt.QVBoxLayout() + self.setLayout(layout) + + # 'flow capacity definitions per depth' Table + self.table = qt.QTableWidget() + self.table.setAlternatingRowColors(True) + self.table.verticalHeader().setVisible(False) + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(self.HEADER) + self.table.horizontalHeader().setSectionResizeMode(qt.QHeaderView.ResizeToContents) + self.table.horizontalHeader().setStretchLastSection(False) + self._tableCellHandler = KdsOptimizationTableCellHandler(self.table) + + # Define the same size for all columns + for i in range(self.table.columnCount): + self.table.horizontalHeader().setSectionResizeMode(i, qt.QHeaderView.Stretch) + + self.table.setSelectionBehavior(qt.QAbstractItemView.SelectRows) + self.table.setSelectionMode(qt.QAbstractItemView.ExtendedSelection) + + self.addButton = qt.QPushButton("Add") + self.addButton.clicked.connect(self.__onAddButtonClicked) + self.removeButton = qt.QPushButton("Remove") + self.removeButton.clicked.connect(self.__onRemoveButtonClicked) + + # Weight value + self.weightSpinBox = ui.numberParam(vrange=(0, sys.float_info.max), value=1.0) + formLayout = qt.QFormLayout() + formLayout.addRow("Weight", self.weightSpinBox) + + # Layout + buttonsLayout = qt.QHBoxLayout() + buttonsLayout.addStretch() + buttonsLayout.addWidget(self.addButton) + buttonsLayout.addWidget(self.removeButton) + + layout.addWidget(self.table) + layout.addLayout(buttonsLayout) + layout.addLayout(formLayout) + + def __onAddButtonClicked(self, checked): + dialog = AddKdsOptimizationItemDialog(self) + dialog.objectName = "Add Kds Optimization Item Dialog" + result = dialog.exec_() + if result == qt.QDialog.Accepted: + state = self.table.blockSignals(True) + self.table.insertRow(self.table.rowCount) + self.table.setItem(self.table.rowCount - 1, 0, qt.QTableWidgetItem(str(dialog.model.initialDepth))) + self.table.setItem(self.table.rowCount - 1, 1, qt.QTableWidgetItem(str(dialog.model.endDepth))) + self.table.setItem(self.table.rowCount - 1, 2, qt.QTableWidgetItem(str(dialog.model.kDst))) + self.table.setItem(self.table.rowCount - 1, 3, qt.QTableWidgetItem(str(dialog.model.kRo))) + self.table.blockSignals(state) + + df = tableWidgetToDataFrame(self.table) + self.tableUpdated.emit(df) + + def __onRemoveButtonClicked(self, checked): + selectedIndexes = self.table.selectedIndexes() + + rowSet = set() + for index in selectedIndexes: + rowSet.add(index.row()) + + if len(rowSet) <= 0: + return + + rowSet = sorted(rowSet, reverse=True) + + for row in rowSet: + self.table.removeRow(row) + + df = tableWidgetToDataFrame(self.table) + self.tableUpdated.emit(df) + self.table.setCurrentCell(-1, -1) + + def setTableData(self, df: pd.DataFrame = None): + self.table.clearContents() + + if df is None: + return + + self.table.setRowCount(0) + for _, row in df.iterrows(): + self.table.insertRow(self.table.rowCount) + self.table.setItem(self.table.rowCount - 1, 0, qt.QTableWidgetItem(str(row.iloc[0]))) + self.table.setItem(self.table.rowCount - 1, 1, qt.QTableWidgetItem(str(row.iloc[1]))) + self.table.setItem(self.table.rowCount - 1, 2, qt.QTableWidgetItem(str(row.iloc[2]))) + self.table.setItem(self.table.rowCount - 1, 3, qt.QTableWidgetItem(str(row.iloc[3]))) + + +class KdsOptimizationTableCellHandler: + """Class to handle Kds Optimization Table cells user's iteraction""" + + START_DEPTH_COLUMN = 0 + STOP_DEPTH_COLUMN = 1 + + def __init__(self, table: qt.QTableWidget) -> None: + self.currentCell = None + self.previousValue = None + self.table = table + self.table.cellChanged.connect(self.__onCellChanged) + self.table.currentCellChanged.connect(self.__onCurrentCellChanged) + + def restore(self) -> None: + """Restore the previous value""" + state = self.table.blockSignals(True) + self.currentCell.setText(self.previousValue) + self.table.blockSignals(state) + + def validateCell(self) -> bool: + """Validate the current cell + + Returns: + bool: True if the cell is valid, otherwise False + """ + if not self.currentCell or not self.currentCell.text(): + return False + + try: + currentValue = float(self.currentCell.text()) + except ValueError: # not a number + return False + + row = self.currentCell.row() + column = self.currentCell.column() + + if column not in [self.START_DEPTH_COLUMN, self.STOP_DEPTH_COLUMN]: + return True # No validation needed for other columns + + if column == 0: # related to 'Start Depth' + stopDepthItem = self.table.item(row, self.STOP_DEPTH_COLUMN) + return currentValue < float(stopDepthItem.text()) + + if column == 1: # related to 'Stop Depth' + startDepthItem = self.table.item(row, self.START_DEPTH_COLUMN) + return currentValue > float(startDepthItem.text()) + + return True + + def __onCellChanged(self, row: int, column: int): + """Handle cell changed event. + + Args: + row (int): the current row. + column (int): the current column. + """ + if self.currentCell is None or self.currentCell.row() != row or self.currentCell.column() != column: + return + + if not self.validateCell(): + self.restore() + return + + self.previousValue = self.currentCell.text() + + def __onCurrentCellChanged(self, currentRow: int, currentColumn: int, previousRow: int, previousColumn: int): + """Handle current cell changed event. Used to store the reference from the selected cell item object. + + Args: + currentRow (int): the current row selected. + currentColumn (int): the current column selected. + previousRow (int): the previous row selected. + previousColumn (int): the previous column selected. + """ + self.currentCell = self.table.item(currentRow, currentColumn) + self.previousValue = self.currentCell.text() if self.currentCell else None diff --git a/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py b/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py index acb0922..55a9ea2 100644 --- a/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py +++ b/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py @@ -1,16 +1,22 @@ -import os - import ctk import markdown2 as markdown import qt import slicer +import logging +import os + from ltrace.slicer import helpers, ui from ltrace.slicer.helpers import highlight_error, reset_style_on_valid_node from ltrace.slicer.helpers import reset_style_on_valid_text from ltrace.slicer.node_attributes import ImageLogDataSelectable from ltrace.slicer.ui import hierarchyVolumeInput +from ltrace.slicer_utils import dataframeFromTable, dataFrameToTableNode from ltrace.slicer.widget.global_progress_bar import LocalProgressBar +from typing import List from vtk.util.numpy_support import vtk_to_numpy +from ImageLogsLib.KdsOptimizationTableWidget import KdsOptimizationWidget + +ERROR_CORRECTION_NODE_NAME = "ERROR_CORRECTION_TABLE_NODE" class PermeabilityModelingWidget(qt.QWidget): @@ -20,6 +26,7 @@ def __init__(self, parent=None): self.logic = PermeabilityModelingLogic(self) self.onOutputNodeReady = lambda volumes: None + self.__kdsOptimizationTableId = None layout = qt.QVBoxLayout() self.setLayout(layout) @@ -36,9 +43,11 @@ def __init__(self, parent=None): self.inputSection() self.paramsSection() self.measurementSection() + self.kdsOptimizationSection() self.outputSection() self.applyButton = ui.ButtonWidget(text="Apply", onClick=self.onApply) + self.applyButton.objectName = "Apply Button" self.applyButton.setStyleSheet("QPushButton {font-size: 11px; font-weight: bold; padding: 8px; margin: 4px}") self.applyButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred) @@ -51,6 +60,8 @@ def __init__(self, parent=None): layout.addStretch(1) + slicer.mrmlScene.AddObserver(slicer.mrmlScene.EndImportEvent, self.__onLoadProject) + @classmethod def help(cls): htmlHelp = "" @@ -64,6 +75,62 @@ def readme_path(cls): dir_path = os.path.dirname(os.path.realpath(__file__)) return str(dir_path + "/" + "PermeabilityModeling.md") + def enter(self) -> None: + super().enter() + self.__updateKdsOptimizationTable() + + def __onLoadProject(self, *args, **kwargs): + try: + nodes = slicer.util.getNodes(ERROR_CORRECTION_NODE_NAME, useLists=True) + nodes: List = list(nodes.values()) if nodes else [] + except slicer.util.MRMLNodeNotFoundException: + self.__updateKdsOptimizationTable() + return + + if len(nodes) <= 0: + self.__kdsOptimizationTableId = None + self.__updateKdsOptimizationTable() + return + + nodes = nodes[0] + if len(nodes) == 1: + node = nodes[0] + self.__kdsOptimizationTableId = node.GetID() + self.__updateKdsOptimizationTable() + return + + # If there is more than one related node, remove the old one and keep the first one in the list + for node in nodes[:]: + if node.GetID() != self.__kdsOptimizationTableId: + continue + + nodes.remove(node) + slicer.mrmlScene.RemoveNode(node) + break + + for node in nodes[1:]: + slicer.mrmlScene.RemoveNode(node) + del node + + self.__kdsOptimizationTableId = nodes[0].GetID() + self.__updateKdsOptimizationTable() + + def __updateKdsOptimizationTable(self): + if not self.kdsOptimizationWidget: + return + + if self.__kdsOptimizationTableId is None: + self.kdsOptimizationWidget.setTableData(None) + return + + node = helpers.tryGetNode(self.__kdsOptimizationTableId) + if node is None: + self.kdsOptimizationWidget.setTableData(None) + return + + df = dataframeFromTable(node) + self.kdsOptimizationWidget.setTableData(df) + def inputSection(self): parametersCollapsibleButton = ctk.ctkCollapsibleButton() parametersCollapsibleButton.text = "Input Images" @@ -74,11 +141,15 @@ def inputSection(self): self._porosity_log_input = hierarchyVolumeInput(onChange=lambda i: self.__on_porosity_table_changed(i)) self._porosity_log_input.setNodeTypes(["vtkMRMLTableNode"]) + self._porosity_log_input.objectName = "Well Logs Input" + reset_style_on_valid_node(self._porosity_log_input) self._porosity_log_combo_box = qt.QComboBox() + self._porosity_log_combo_box.objectName = "Porosity Log Combo Box" reset_style_on_valid_node(self._porosity_log_combo_box) self.segmented_image_input = hierarchyVolumeInput(onChange=self.onSegmentedImageSelected) self.segmented_image_input.setNodeTypes(["vtkMRMLSegmentationNode", "vtkMRMLLabelMapVolumeNode"]) + self.segmented_image_input.objectName = "Segmented Image Input" reset_style_on_valid_node(self.segmented_image_input) parametersFormLayout.addRow("Well logs (.las):", self._porosity_log_input) @@ -115,10 +186,12 @@ def paramsSection(self): self.modelSelector = qt.QComboBox() self.modelSelector.enabled = False self.modelSelector.connect("currentIndexChanged(int)", lambda v: self.onNumericChanged("class1", v)) + self.modelSelector.objectName = "Macro Pore Segment Combo Box" self.missingSelector = qt.QComboBox() self.missingSelector.enabled = False self.missingSelector.connect("currentIndexChanged(int)", lambda v: self.onNumericChanged("nullable", v)) + self.missingSelector.objectName = "Ignored/null Segment Combo Box" parametersFormLayout.addRow("Macro Pore Segment: ", self.modelSelector) parametersFormLayout.addRow("Ignored/null Segment: ", self.missingSelector) @@ -136,10 +209,40 @@ def measurementSection(self): ) reset_style_on_valid_node(self._plugs_permeability_table_combo_box) self._plugs_permeability_table_combo_box.setNodeTypes(["vtkMRMLTableNode"]) + self._plugs_permeability_table_combo_box.objectName = "Plugs Measurements Input" self._plugs_permeability_log_combo_box = qt.QComboBox() + self._plugs_permeability_log_combo_box.objectName = "Plugs Permeability Log Combo Box" parametersFormLayout.addRow("Plugs measurements:", self._plugs_permeability_table_combo_box) parametersFormLayout.addRow("Plugs Permeability Log:", self._plugs_permeability_log_combo_box) + def kdsOptimizationSection(self): + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Kds Optimization" + self.layout().addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QVBoxLayout(parametersCollapsibleButton) + + self.kdsOptimizationWidget = KdsOptimizationWidget() + self.kdsOptimizationWidget.tableUpdated.connect(self.storeKdsOptimizationTable) + self.kdsOptimizationWidget.objectName = "Kds Optimization Widget" + self.kdsOptimizationWidget.table.objectName = "Kds Optimization Table" + self.kdsOptimizationWidget.addButton.objectName = "Kds Optimization Add Button" + self.kdsOptimizationWidget.removeButton.objectName = "Kds Optimization Remove Button" + self.kdsOptimizationWidget.weightSpinBox.objectName = "Kds Optimization Weight Spin Box" + parametersFormLayout.addWidget(self.kdsOptimizationWidget) + + def storeKdsOptimizationTable(self, df): + node = helpers.tryGetNode(ERROR_CORRECTION_NODE_NAME) + if node is None: + node = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLTableNode.__name__, ERROR_CORRECTION_NODE_NAME) + node.SetHideFromEditors(True) + + node.RemoveAllColumns() + dataFrameToTableNode(df, node) + node.Modified() + self.__kdsOptimizationTableId = node.GetID() + def __on_plugs_permeability_table_changed(self, node_id): node = self.subjectHierarchyNode.GetItemDataNode(node_id) @@ -168,6 +271,7 @@ def outputSection(self): self.outputNameLineEdit = qt.QLineEdit() reset_style_on_valid_text(self.outputNameLineEdit) self.outputNameLineEdit.setToolTip("Type the text to be used as the output node's name.") + self.outputNameLineEdit.objectName = "Output Name Line Edit" # Layout within the dummy collapsible button parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) @@ -203,6 +307,9 @@ def onApply(self): self.logic.model["outputVolumeName"] = self.outputNameLineEdit.text # Update model parameters + + kdsOptimizationTable = helpers.tryGetNode(ERROR_CORRECTION_NODE_NAME) + porosity_log_scalar_node = helpers.createTemporaryVolumeNode( slicer.vtkMRMLScalarVolumeNode, "POROSITY_LOG_TMP_NODE", hidden=True ) @@ -255,6 +362,10 @@ def onApply(self): self.logic.model["perm_plugs"] = permeability_plug_log_scalar_node.GetID() self.logic.model["depth_plugs"] = permeability_plug_depth_scalar_node.GetID() self.logic.model["outputVolume"] = permeability_output.GetID() + self.logic.model["kdsOptimizationTable"] = ( + kdsOptimizationTable.GetID() if kdsOptimizationTable is not None else None + ) + self.logic.model["kdsOptimizationWeight"] = self.kdsOptimizationWidget.weightSpinBox.value self.logic.run() @@ -350,7 +461,7 @@ def eventHandler(self, caller, event): self.widget.onCancelled() self.cliNode = None except Exception as e: - print(f'Exception on Event Handler: {repr(e)} with status "{status}"') + logging.info(f'Exception on Event Handler: {repr(e)} with status "{status}"') self.widget.onCompletedWithErrors() def run(self): @@ -377,4 +488,6 @@ def PermeabilityModelingModel(): outputVolume=None, nullable=0, outputVolumeName=None, + kdsOptimizationTable=None, + kdsOptimizationWeight=None, ) diff --git a/tools/__init__.py b/src/modules/ImageLogEnv/ImageLogsLib/__init__.py similarity index 100% rename from tools/__init__.py rename to src/modules/ImageLogEnv/ImageLogsLib/__init__.py diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py index 3222077..1729d56 100644 --- a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py +++ b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py @@ -10,6 +10,7 @@ import pathlib import sys +import numpy as np import lasio import mrml @@ -22,7 +23,7 @@ from scipy.optimize import minimize from ltrace.slicer.helpers import getDepthArrayFromVolume -from ltrace.slicer.cli_utils import writeToTable +from ltrace.slicer.cli_utils import writeToTable, readFrom from PermeabilityModelingLib import * @@ -34,12 +35,47 @@ def progressUpdate(value): sys.stdout.flush() -def readFrom(volumeFile, builder): - sn = slicer.vtkMRMLNRRDStorageNode() - sn.SetFileName(volumeFile) - nodeIn = builder() - sn.ReadData(nodeIn) # read data from volumeFile into nodeIn - return nodeIn +def dataframeFromTable(tableNode): + """Optimized version from slicer.util.dataframeFromTable + + Convert table node content to pandas dataframe. + + Table content is copied. Therefore, changes in table node do not affect the dataframe, + and dataframe changes do not affect the original table node. + """ + try: + # Suppress "lzma compression not available" UserWarning when loading pandas + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=UserWarning) + import pandas as pd + except ImportError: + raise ImportError( + "Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`" + ) + + vtable = tableNode.GetTable() + data = [] + columns = [] + for columnIndex in range(vtable.GetNumberOfColumns()): + vcolumn = vtable.GetColumn(columnIndex) + numberOfComponents = vcolumn.GetNumberOfComponents() + column_name = vcolumn.GetName() + columns.append(column_name) + + if numberOfComponents == 1: + column_data = [vcolumn.GetValue(rowIndex) for rowIndex in range(vcolumn.GetNumberOfValues())] + else: + column_data = [] + for rowIndex in range(vcolumn.GetNumberOfTuples()): + item = [vcolumn.GetValue(rowIndex, componentIndex) for componentIndex in range(numberOfComponents)] + column_data.append(tuple(item)) + data.append(column_data) + + dataframe = pd.DataFrame(zip(*data), columns=columns) + + return dataframe if __name__ == "__main__": @@ -51,6 +87,17 @@ def readFrom(volumeFile, builder): parser.add_argument("--master1", type=str, dest="inputVolume1", required=True, help="Amplitude image log") parser.add_argument("--depth_plugs", type=str, dest="depth_plugs", required=True, help="Amplitude image log") parser.add_argument("--perm_plugs", type=str, dest="perm_plugs", required=True, help="Amplitude image log") + parser.add_argument( + "--kdsOptimizationTable", dest="kdsOptimizationTable", required=False, help="Kds Optimization Table" + ) + parser.add_argument( + "--kdsOptimizationWeight", + type=float, + dest="kdsOptimizationWeight", + required=False, + help="Kds Optimization Weight", + ) + parser.add_argument( "--outputvolume", type=str, dest="outputVolume", default=None, help="Output labelmap (3d) Values" ) @@ -69,11 +116,11 @@ def readFrom(volumeFile, builder): ## LOAD DATA progressUpdate(value=0.0) # Read as slicer node (copy) - porosity_las_vol = readFrom(args.log_por, mrml.vtkMRMLScalarVolumeNode) - porosity_depth_las_vol = readFrom(args.depth_por, mrml.vtkMRMLScalarVolumeNode) - segmentation = readFrom(args.inputVolume1, mrml.vtkMRMLScalarVolumeNode) - depth_plugs_vol = readFrom(args.depth_plugs, mrml.vtkMRMLScalarVolumeNode) - permebility_plugs_vol = readFrom(args.perm_plugs, mrml.vtkMRMLScalarVolumeNode) + porosity_las_vol = readFrom(args.log_por, slicer.vtkMRMLScalarVolumeNode) + porosity_depth_las_vol = readFrom(args.depth_por, slicer.vtkMRMLScalarVolumeNode) + segmentation = readFrom(args.inputVolume1, slicer.vtkMRMLScalarVolumeNode) + depth_plugs_vol = readFrom(args.depth_plugs, slicer.vtkMRMLScalarVolumeNode) + permebility_plugs_vol = readFrom(args.perm_plugs, slicer.vtkMRMLScalarVolumeNode) # Access numpy view (reference) porosity_las = slicer.util.arrayFromVolume(porosity_las_vol).squeeze() @@ -84,6 +131,13 @@ def readFrom(volumeFile, builder): permebility_plugs = slicer.util.arrayFromVolume(permebility_plugs_vol) permebility_plugs[permebility_plugs < 0.001] = 0.001 + kdsOptimizationDataFrame = None + if args.kdsOptimizationTable is not None: + kdsOptimizationTable = readFrom( + args.kdsOptimizationTable, slicer.vtkMRMLTableNode, slicer.vtkMRMLTableStorageNode + ) + kdsOptimizationDataFrame = dataframeFromTable(kdsOptimizationTable) + # Mantain segmentation image in ascending order if depth_image_array[0] > depth_image_array[-1]: depth_image_array[:] = np.flipud(depth_image_array) @@ -143,6 +197,9 @@ def readFrom(volumeFile, builder): segment_list, np.array(porosity_2opt, np.double), ids_, + depth_2opt, + kdsOptimizationDataFrame, + args.kdsOptimizationWeight, ), method="SLSQP", bounds=bnds, @@ -151,10 +208,28 @@ def readFrom(volumeFile, builder): print(res.x) error_initial = objective_funcion( - perm_parameters, permebility_plugs, proportions_2opt, segment_list, porosity_2opt, ids_ + perm_parameters, + permebility_plugs, + proportions_2opt, + segment_list, + porosity_2opt, + ids_, + depth_2opt, + kdsOptimizationDataFrame, + args.kdsOptimizationWeight, ) print("Initial error: ", error_initial) - error_final = objective_funcion(res.x, permebility_plugs, proportions_2opt, segment_list, porosity_2opt, ids_) + error_final = objective_funcion( + res.x, + permebility_plugs, + proportions_2opt, + segment_list, + porosity_2opt, + ids_, + depth_2opt, + kdsOptimizationDataFrame, + args.kdsOptimizationWeight, + ) print("Optimized error: ", error_final) progressUpdate(value=0.9) diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml index 7c9a612..068781e 100644 --- a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml +++ b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml @@ -71,6 +71,27 @@ + + + + kdsOptimizationTable + kdsOptimizationTable + + Table with parameters related to the Kds Optimization equation per depth interval + input + +
+ + + kdsOptimizationWeight + kdsOptimizationWeight + + Kds Optimization's weight value + input + 1 + +
+ outputVolume diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py index 5fb7f32..3c72762 100644 --- a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py +++ b/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py @@ -4,10 +4,11 @@ @author: leandro """ - - import numpy as np -import csv +import pandas as pd + +from dataclasses import dataclass +from typing import List def compute_segment_proportion_array(segmentation, id_segment_null): @@ -36,15 +37,16 @@ def compute_segment_proportion_array(segmentation, id_segment_null): n_lines = np.shape(segmentation)[0] proportions = np.zeros((n_lines, len(segment_list))) - segment_index = 0 - for segment in segment_list: + for segment_index, segment in enumerate(segment_list): auxiliar_array2count = np.zeros(np.shape(segmentation)) auxiliar_array2count[segmentation == segment] = 1 aux = np.sum(auxiliar_array2count, axis=1) - proportions[:, segment_index] = np.sum(aux, axis=1) + segmentProportion = np.sum(aux, axis=1) total_valid_pixels = (np.shape(segmentation)[2] * np.shape(segmentation)[1]) - n_null_values - proportions[:, segment_index] = proportions[:, segment_index] / total_valid_pixels - segment_index = segment_index + 1 + result = np.divide( + segmentProportion, total_valid_pixels, out=np.zeros_like(segmentProportion), where=total_valid_pixels != 0 + ) + proportions[:, segment_index] = result return proportions, segment_list @@ -93,10 +95,118 @@ def compute_permeability(proportions, segment_list, porosity_array, perm_paramet return permeability -def objective_funcion(perm_parameters, permebility_plugs, proportions_2opt, segment_list, porosity_2opt, ids): - +def objective_funcion( + perm_parameters, + permebility_plugs, + proportions_2opt, + segment_list, + porosity_2opt, + ids, + depthArray: np.ndarray, + kdsOptimizationDataFrame: pd.DataFrame = None, + kdsOptimizationWeight=None, +) -> float: permeability_2opt = compute_permeability(proportions_2opt, segment_list, porosity_2opt, perm_parameters, ids) - error = np.sum(np.power(np.log10(permebility_plugs) - np.log10(permeability_2opt), 2)) + hasKdsOptimization = len(kdsOptimizationDataFrame.index) > 0 if kdsOptimizationDataFrame is not None else False + if ( + hasKdsOptimization + and kdsOptimizationDataFrame is not None + and kdsOptimizationWeight is not None + and len(permebility_plugs) > 0 + ): + error = np.sqrt(error) / len(permeability_2opt) + kdsOptError = kdsOptimizationTerm( + depthArray=depthArray, + kiArray=permeability_2opt, + kdsOptimizationDataFrame=kdsOptimizationDataFrame, + kdsOptimizationWeight=kdsOptimizationWeight, + ) + error += kdsOptError + return error + + +@dataclass +class KdsOptimization: + startDepth: float + stopDepth: float + kDst: float + kRro: float + depthInterval: float = None + + def __post_init__(self): + self.depthInterval = self.stopDepth - self.startDepth + + def calculateKiH(self, kiArray: np.ndarray, depthArray: np.ndarray): + if len(kiArray) != len(depthArray): + raise ValueError("kiArray and depthArray must have the same length") + + # Trim depthArray in interval valid for startDepth and stopDepth + validDepthArrayIndexes = np.argwhere((depthArray >= self.startDepth) & (depthArray <= self.stopDepth)) + + if len(validDepthArrayIndexes) == 0 or len(depthArray) <= 1 or len(kiArray) <= 1: + return 0.0 + + trimmedDepthArray = depthArray[validDepthArrayIndexes] + minDepth = depthArray[0] + startDepth = trimmedDepthArray[0].item() + + # Adjusting startDepth to be the first valid depth in interval + if minDepth <= self.startDepth: + startDepth = min(self.startDepth, startDepth) + + stopDepth = trimmedDepthArray[-1].item() + # Adjusting start/stop depth in case there is only one valid depth value in interval + if len(trimmedDepthArray) == 1: + startDepth = min(trimmedDepthArray[0].item(), self.startDepth) + stopDepth = min(stopDepth, self.stopDepth) + trimmedDepthArray = np.array([startDepth, stopDepth]) + ki = kiArray[validDepthArrayIndexes[0]].item() + trimmedKiArray = np.array([ki, ki]) + + trimmedKiArray = kiArray[validDepthArrayIndexes] + trimmedDepthArray[0] = startDepth + kiAvg = np.mean(trimmedKiArray) + kiH = kiAvg * abs(stopDepth - startDepth) + + return kiH.item() + + def conflicts(self, other: "KdsOptimization") -> bool: + return self.startDepth <= other.stopDepth and self.stopDepth >= other.startDepth + + +def kdsOptimizationTerm( + depthArray: np.ndarray, kiArray: np.ndarray, kdsOptimizationDataFrame: pd.DataFrame, kdsOptimizationWeight: float +) -> float: + kdsOptimizationIntervals: List[KdsOptimization] = [] + for _, row in kdsOptimizationDataFrame.iterrows(): + startDepth = float(row.iloc[0]) + stopDepth = float(row.iloc[1]) + kDst = float(row.iloc[2]) + kRro = float(row.iloc[3]) + + kdsOptimizationIntervals.append(KdsOptimization(startDepth, stopDepth, kDst, kRro)) + + errorValues = [] + hTotal = 0 + for interval in kdsOptimizationIntervals: + if interval.kRro == 0: + continue + + kiH = interval.calculateKiH(kiArray, depthArray) + kDst_kRro = interval.kDst / interval.kRro + if np.isclose(kiH, 0) or np.isclose(kDst_kRro, 0): + continue + + error = np.power(np.log10(kiH) - np.log10(kDst_kRro), 2) + errorValues.append(error) + hTotal += interval.depthInterval + + if hTotal == 0: + return 0.0 + + errorSum = (kdsOptimizationWeight / hTotal) * np.sqrt(np.sum(errorValues)) + + return errorSum diff --git a/src/modules/ImageLogExport/ImageLogExportLib/ImageLogCSV.py b/src/modules/ImageLogExport/ImageLogExportLib/ImageLogCSV.py index 52e1efb..a44a7c5 100644 --- a/src/modules/ImageLogExport/ImageLogExportLib/ImageLogCSV.py +++ b/src/modules/ImageLogExport/ImageLogExportLib/ImageLogCSV.py @@ -1,4 +1,5 @@ import csv +import re import numpy as np import pandas as pd import slicer @@ -92,7 +93,10 @@ def exportCSV(node: slicer.vtkMRMLNode, directory: Path, isTechlog: bool = False if values.ndim == 1: values = values[:, np.newaxis] - filename = directory / f"{node.GetName()}.csv" + fileName = node.GetName() + fileName = re.sub(r"[\\/*.<>ç?:]", "_", fileName) # avoiding characters not suitable for file name + + filename = directory / f"{fileName}.csv" with open(filename, mode="w", newline="") as csvFile: writer = csv.writer(csvFile) diff --git a/src/modules/ImageLogExport/ImageLogExportLib/widgets/ImageLogExportOpenSourceWidget.py b/src/modules/ImageLogExport/ImageLogExportLib/widgets/ImageLogExportOpenSourceWidget.py index d199b06..277b8e7 100644 --- a/src/modules/ImageLogExport/ImageLogExportLib/widgets/ImageLogExportOpenSourceWidget.py +++ b/src/modules/ImageLogExport/ImageLogExportLib/widgets/ImageLogExportOpenSourceWidget.py @@ -1,12 +1,18 @@ import logging import vtk, qt, ctk, slicer - +import json +import os +import re +from .output_name_dialog import OutputNameDialog from pathlib import Path from Export import ExportLogic, checkUniqueNames from ImageLogExportLib import ImageLogCSV from ltrace.slicer.helpers import getNodeDataPath from ltrace.slicer_utils import LTracePluginWidget from ltrace.utils.recursive_progress import RecursiveProgress +import ltrace.image.las as imglas +from ltrace.slicer.helpers import createTemporaryNode, getNodeDataPath, getSourceVolume, removeTemporaryNodes +from ltrace.slicer.node_attributes import NodeEnvironment, TableType class ImageLogExportOpenSourceWidget(LTracePluginWidget): @@ -15,6 +21,8 @@ class ImageLogExportOpenSourceWidget(LTracePluginWidget): FORMAT_MATRIX_CSV = "CSV (matrix format)" FORMAT_TECHLOG_CSV = "CSV (Techlog format)" FORMAT_CSV = "CSV" + FORMAT_LAS = "LAS" + FORMAT_LAS_GEOLOG = "LAS (for Geolog)" EXPORTABLE_TYPES = ( slicer.vtkMRMLSegmentationNode, @@ -23,8 +31,8 @@ class ImageLogExportOpenSourceWidget(LTracePluginWidget): slicer.vtkMRMLLabelMapVolumeNode, ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, parent) -> None: + super().__init__(parent) self.cancel = False self.cliCompleted = False self.auxNode = None @@ -44,6 +52,8 @@ def setup(self): self.logFormatBox = qt.QComboBox() self.logFormatBox.addItem(ImageLogExportOpenSourceWidget.FORMAT_MATRIX_CSV) self.logFormatBox.addItem(ImageLogExportOpenSourceWidget.FORMAT_TECHLOG_CSV) + self.logFormatBox.addItem(ImageLogExportOpenSourceWidget.FORMAT_LAS) + self.logFormatBox.addItem(ImageLogExportOpenSourceWidget.FORMAT_LAS_GEOLOG) self.tableFormatBox = qt.QComboBox() self.tableFormatBox.addItem(ImageLogExportOpenSourceWidget.FORMAT_CSV) @@ -153,8 +163,19 @@ def onExportClicked(self): # Separate nodes nodeToExportList = [] + nodeToLASList = [] for node in self.nodes: - nodeToExportList.append(node) + if ( + type(node) is slicer.vtkMRMLTableNode + or self.logFormatBox.currentText == ImageLogExportClosedSourceWidget.FORMAT_MATRIX_CSV + or self.logFormatBox.currentText == ImageLogExportClosedSourceWidget.FORMAT_TECHLOG_CSV + ): + nodeToExportList.append(node) + elif ( + self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS + or self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS_GEOLOG + ): + nodeToLASList.append(node) # Create progress management def progressCallback(progressValue): @@ -162,8 +183,11 @@ def progressCallback(progressValue): baseProgress = RecursiveProgress(callback=progressCallback) progressList = [] + lasProgress = None for node in nodeToExportList: progressList.append(baseProgress.create_sub_progress()) + if len(nodeToLASList) > 0: + lasProgress = baseProgress.create_sub_progress(weight=len(nodeToLASList)) # Export for node in nodeToExportList: @@ -171,19 +195,125 @@ def progressCallback(progressValue): progress = progressList.pop() if type(node) is slicer.vtkMRMLTableNode: - if self.tableFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_CSV: - ExportLogic().exportTable(node, outputDir, nodeDir, ExportLogic.TABLE_FORMAT_CSV) - progress.set_progress(1) + if node.GetAttribute(TableType.name()) == TableType.HISTOGRAM_IN_DEPTH.value: + if self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_MATRIX_CSV: + self.startLogToCsvExport(node, nodeDir, progress, isTechlog=False) + else: + if node in nodeToLASList: + logging.warning( + f"If you want {node.GetName()} to be exported in a separated CSV file, select {ImageLogExportOpenSourceWidget.FORMAT_MATRIX_CSV} option in the Well log and export again." + ) + else: + logging.warning( + f"{node.GetName()} not exported as CSV. Please select {ImageLogExportOpenSourceWidget.FORMAT_MATRIX_CSV} option and try to export it again." + ) + else: + if self.tableFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_CSV: + ExportLogic().exportTable(node, outputDir, nodeDir, ExportLogic.TABLE_FORMAT_CSV) + progress.set_progress(1) else: if self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_TECHLOG_CSV: self.startLogToCsvExport(node, nodeDir, progress, isTechlog=True) elif self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_MATRIX_CSV: self.startLogToCsvExport(node, nodeDir, progress, isTechlog=False) + elif ( + self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS + or self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS_GEOLOG + ): + raise RuntimeError("Node selection went wrong") + + if ( + self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS + or self.logFormatBox.currentText == ImageLogExportOpenSourceWidget.FORMAT_LAS_GEOLOG + ) and nodeToLASList: + try: + self.startLasExport(nodeToLASList, outputDir, lasProgress) + except RuntimeError as e: + logging.error(e) + self._stopExport() + self.progressBar.setValue(0) + self.currentStatusLabel.text = "Export failed!" + return self._stopExport() self.progressBar.setValue(100) self.currentStatusLabel.text = "Export complete" + def preExport(self, node_list): + # Get file ID + nodePath = Path() + fileId = "" + if len(node_list) == 1: + fileId = node_list[0].GetName() + nodePath = self.__getNodeDirectoryPath(node_list[0]) + elif len(node_list) > 0: + directoryName = self.__getNodeDirectoryName(node_list[0]) + askForOutputName = False + for node in node_list: + newDirectoryName = self.__getNodeDirectoryName(node) + directoryPath = self.__getNodeDirectoryPath(node_list[0]) + if directoryName != newDirectoryName: + askForOutputName = True + break + if askForOutputName: + outputNameDialog = OutputNameDialog(self.layout.parentWidget()) + result = outputNameDialog.exec() + if bool(result): + fileId = outputNameDialog.getOutputName() + else: + return None, None + else: + nodePath = directoryPath + fileId = directoryName + + fileId = re.sub(r"[\\/*.<>ç?:]", "_", fileId) # avoiding characters not suitable for file name + + return nodePath, fileId + + def startLasExport(self, node_list, output_dir, progressOutput): + nodePath, fileId = self.preExport(node_list) + if not nodePath: + return False + outputDir = Path(output_dir) + tempDir = slicer.app.temporaryPath + "/imagelogexport/" + if not os.path.exists(tempDir): + os.makedirs(tempDir) + + nodeList2 = [] # substitutes vtkMRMLSegmentationNodes by temporary LabelMaps + for i, node in enumerate(node_list): + if isinstance(node, slicer.vtkMRMLSegmentationNode): + auxNode = createTemporaryNode(slicer.vtkMRMLLabelMapVolumeNode, "__temp__") + referenceVolumeNode = getSourceVolume(node) + if not slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode( + node, auxNode, referenceVolumeNode + ): + errmsg = f"Export of segment failed for node {auxNode.GetName()}." + raise RuntimeError(errmsg) + nodeList2.append(auxNode) + else: + nodeList2.append(node) + + if not self.ignoreDirStructureCheckbox.checked: + outpuPath = outputDir / nodePath + else: + outpuPath = outputDir + outpuPath.mkdir(parents=True, exist_ok=True) + + outputFilePath: Path = outpuPath / f"{fileId}.las" + outputFilePathStr: str = outputFilePath.as_posix() + + # "FORMAT_LAS_GEOLOG" is in fact an initial support to LAS 3.0 + version = 2 if self.logFormatBox.currentText != ImageLogExportOpenSourceWidget.FORMAT_LAS_GEOLOG else 3 + + try: + imglas.export_las(nodeList2, outputFilePathStr, version=version) + except RuntimeError as e: + raise RuntimeError(e) + finally: + removeTemporaryNodes(NodeEnvironment.IMAGE_LOG) + + return True + def onCancelClicked(self): self.cancel = True @@ -202,3 +332,21 @@ def startLogToCsvExport(self, node, nodeDir, progressOutput, isTechlog): self._stopExport() self.currentStatusLabel.text = "Export failed." raise exc + + @staticmethod + def __getNodeDirectoryName(node): + subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + itemParent = subjectHierarchyNode.GetItemParent(subjectHierarchyNode.GetItemByDataNode(node)) + directoryName = subjectHierarchyNode.GetItemName(itemParent) + return directoryName + + @staticmethod + def __getNodeDirectoryPath(node): + directoryPath = Path() + subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + sceneItemId = subjectHierarchyNode.GetSceneItemID() + itemParent = subjectHierarchyNode.GetItemParent(subjectHierarchyNode.GetItemByDataNode(node)) + while itemParent != 0 and itemParent != sceneItemId: + directoryPath = Path(subjectHierarchyNode.GetItemName(itemParent)) / directoryPath + itemParent = subjectHierarchyNode.GetItemParent(itemParent) + return directoryPath diff --git a/src/modules/ImageLogExport/ImageLogExportLib/widgets/output_name_dialog.py b/src/modules/ImageLogExport/ImageLogExportLib/widgets/output_name_dialog.py new file mode 100644 index 0000000..2c7046e --- /dev/null +++ b/src/modules/ImageLogExport/ImageLogExportLib/widgets/output_name_dialog.py @@ -0,0 +1,41 @@ +import qt + + +class OutputNameDialog(qt.QDialog): + def __init__(self, parent): + super().__init__(parent) + self.setWindowFlags(self.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint) + self.setWindowTitle("DLIS output name") + self.setFixedSize(200, 60) + + formLayout = qt.QFormLayout() + self.newPlotWidgetLineEdit = qt.QLineEdit() + formLayout.addRow("File name", self.newPlotWidgetLineEdit) + + okButton = qt.QPushButton("OK") + cancelButton = qt.QPushButton("Cancel") + + okButton.clicked.connect(lambda checked: self.okButtonClicked()) + cancelButton.clicked.connect(lambda checked: self.reject()) + + buttonsLayout = qt.QHBoxLayout() + buttonsLayout.addWidget(okButton) + buttonsLayout.addWidget(cancelButton) + formLayout.addRow(buttonsLayout) + formLayout.setVerticalSpacing(10) + + self.setLayout(formLayout) + + def showPopup(self, message): + qt.QMessageBox.warning(self, "Error", message) + + def okButtonClicked(self): + newPlotLabel = self.newPlotWidgetLineEdit.text + if newPlotLabel == "": + self.showPopup("File name cannot be empty") + return + + self.accept() + + def getOutputName(self): + return self.newPlotWidgetLineEdit.text diff --git a/src/modules/ImageLogInpaint/CustomizedWidget/RenameDialog.py b/src/modules/ImageLogInpaint/CustomizedWidget/RenameDialog.py new file mode 100644 index 0000000..8061b87 --- /dev/null +++ b/src/modules/ImageLogInpaint/CustomizedWidget/RenameDialog.py @@ -0,0 +1,51 @@ +import qt +from Customizer import Customizer + + +class RenameDialog(qt.QDialog): + def __init__(self, parent): + super().__init__(parent) + self.setWindowFlags(self.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint) + self.setWindowTitle("Rename Volume") + + self.newPlotWidgetLineEdit = qt.QLineEdit() + + okButton = qt.QPushButton("OK") + okButton.setIcon(qt.QIcon(str(Customizer.APPLY_ICON_PATH))) + okButton.setIconSize(qt.QSize(12, 14)) + + cancelButton = qt.QPushButton("Cancel") + cancelButton.setIcon(qt.QIcon(str(Customizer.CANCEL_ICON_PATH))) + cancelButton.setIconSize(qt.QSize(12, 14)) + + okButton.clicked.connect(lambda checked: self.okButtonClicked()) + cancelButton.clicked.connect(lambda checked: self.reject()) + + buttonsLayout = qt.QHBoxLayout() + buttonsLayout.addWidget(okButton) + buttonsLayout.addWidget(cancelButton) + + formLayout = qt.QFormLayout() + formLayout.addRow("New name:", self.newPlotWidgetLineEdit) + formLayout.addRow(buttonsLayout) + formLayout.setVerticalSpacing(10) + + self.setLayout(formLayout) + + def showPopup(self, message): + qt.QMessageBox.warning(self, "Error", message) + + def okButtonClicked(self): + newPlotLabel = self.newPlotWidgetLineEdit.text + + if newPlotLabel == "": + self.showPopup("Volume name cannot be empty") + return + + self.accept() + + def getOutputName(self): + return self.newPlotWidgetLineEdit.text + + def setOutputName(self, name): + self.newPlotWidgetLineEdit.text = name diff --git a/src/modules/ImageLogInpaint/ImageLogInpaint.py b/src/modules/ImageLogInpaint/ImageLogInpaint.py new file mode 100644 index 0000000..7163bbf --- /dev/null +++ b/src/modules/ImageLogInpaint/ImageLogInpaint.py @@ -0,0 +1,558 @@ +import ctk +import os +import qt +import slicer +import numpy as np +import vtk +import slicer.util +import json + +from ltrace.slicer import ui +from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic +from ltrace.slicer.helpers import ( + createTemporaryVolumeNode, + getCurrentEnvironment, + moveNodeTo, + clone_volume, + tryGetNode, + NodeEnvironment, +) +from ltrace.constants import ImageLogInpaintConst +from CustomizedWidget.RenameDialog import RenameDialog +from pathlib import Path +from typing import Dict +from slicer.parameterNodeWrapper import parameterNodeWrapper +from CoreInpaint import PatchMatch + +try: + from Test.ImageLogInpaintTest import ImageLogInpaintTest +except ImportError: + ImageLogInpaintTest = None # tests not deployed to final version or closed source + + +class ImageLogInpaint(LTracePlugin): + SETTING_KEY = "ImageLogInpaint" + MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__))) + + def __init__(self, parent): + LTracePlugin.__init__(self, parent) + self.parent.title = "Image Log Inpaint" + self.parent.categories = ["LTrace Tools"] + self.parent.contributors = ["LTrace Geophysics Team"] + self.parent.helpText = ImageLogInpaint.help() + + @classmethod + def readme_path(cls): + return str(cls.MODULE_DIR / "README.md") + + +@parameterNodeWrapper +class ImageLogInpaintParameterNode: + """ + Class to store/retrieve parametes when the project is saved/loaded. + """ + + segmentations: Dict[slicer.vtkMRMLScalarVolumeNode, slicer.vtkMRMLSegmentationNode] + lastSourceNode: slicer.vtkMRMLScalarVolumeNode + + +class ImageLogInpaintWidget(LTracePluginWidget): + def __init__(self, parent): + LTracePluginWidget.__init__(self, parent) + self.logic = ImageLogInpaintLogic() + self.parameterNode = None + self.inpaintArea = 200 + + self._historySize = 10 + self._volumeHistory = [] + self._segmentationHistory = [] + self._historyPointer = 0 + + # Temp segmentation is used receive the segment drawn by the user. + # After each interation its content is cleared. This allow us to get each segment separately for inpainting. + self._tempSegmentation = None + self._tempLabelMap = None + + self._observerTags = {} + + def setup(self): + LTracePluginWidget.setup(self) + + self.customizedSegmentEditorWidget = slicer.modules.customizedsegmenteditor.createNewWidgetRepresentation() + self.customizedSegmentEditorWidget.self().selectParameterNodeByTag(ImageLogInpaint.SETTING_KEY) + + self.segmentEditorWidget = self.customizedSegmentEditorWidget.self().editor + self.segmentEditorWidget.setEffectNameOrder(["Scissors"]) + self.segmentEditorWidget.unorderedEffectsVisible = False + self.segmentEditorWidget.setAutoShowSourceVolumeNode(False) + + self.segmentEditorWidget.findChild(ctk.ctkMenuButton, "Show3DButton").setVisible(False) + self.segmentEditorWidget.findChild(qt.QLabel, "SourceVolumeNodeLabel").setText("Input image: ") + self.segmentEditorWidget.findChild(qt.QLabel, "SegmentationNodeLabel").setVisible(False) + self.segmentEditorWidget.findChild(qt.QPushButton, "AddSegmentButton").setVisible(False) + self.segmentEditorWidget.findChild(qt.QPushButton, "RemoveSegmentButton").setVisible(False) + + undoButtonWidget = self.segmentEditorWidget.findChild(qt.QToolButton, "UndoButton") + redoButtonWidget = self.segmentEditorWidget.findChild(qt.QToolButton, "RedoButton") + + undoButtonWidget.visible = False + redoButtonWidget.visible = False + + self.undoButton = qt.QToolButton() + self.undoButton.objectName = "undoButtonInpaint" + self.undoButton.setText("Undo") + self.undoButton.enabled = False + self.undoButton.setToolTip(undoButtonWidget.toolTip) + self.undoButton.setToolButtonStyle(undoButtonWidget.toolButtonStyle) + self.undoButton.setIcon(undoButtonWidget.icon) + self.undoButton.setIconSize(undoButtonWidget.iconSize) + self.undoButton.clicked.connect(self.onUndoClicked) + + self.redoButton = qt.QToolButton() + self.redoButton.objectName = "redoButtonInpaint" + self.redoButton.setText("Redo") + self.redoButton.enabled = False + self.redoButton.setToolTip(redoButtonWidget.toolTip) + self.redoButton.setToolButtonStyle(redoButtonWidget.toolButtonStyle) + self.redoButton.setIcon(redoButtonWidget.icon) + self.redoButton.setIconSize(redoButtonWidget.iconSize) + self.redoButton.clicked.connect(self.onRedoClicked) + + self.segmentEditorWidget.findChild(qt.QFrame, "UndoRedoGroupBox").layout().addWidget(self.undoButton, 0, 2) + self.segmentEditorWidget.findChild(qt.QFrame, "UndoRedoGroupBox").layout().addWidget(self.redoButton, 0, 3) + + self.sourceVolumeComboBox = self.segmentEditorWidget.findChild( + slicer.qMRMLNodeComboBox, "SourceVolumeNodeComboBox" + ) + self.sourceVolumeComboBox.objectName = "sourceVolumeNodeComboBox" + self.sourceVolumeComboBox.showChildNodeTypes = False + self.sourceVolumeComboBox.noneDisplay = "Select the image log for inpaint" + self.sourceVolumeComboBox.currentNodeChanged.connect(self.onSourceVolumeChanged) + + self.segmentationComboBox = self.segmentEditorWidget.findChild( + slicer.qMRMLNodeComboBox, "SegmentationNodeComboBox" + ) + self.segmentationComboBox.objectName = "segmentationNodeComboBox" + self.segmentationComboBox.setVisible(False) + + self.cloneButton = ui.ButtonWidget( + text="Clone Volume", + tooltip="Clone the input image", + object_name="cloneVolumeButton", + enabled=False, + onClick=self.onCloneClicked, + ) + + self.renameButton = ui.ButtonWidget( + text="Rename Volume", + tooltip="Rename the input image", + object_name="renameVolumeButton", + enabled=False, + onClick=self.onRenameClicked, + ) + + self.renameDialog = RenameDialog(self.layout.parentWidget()) + self.renameDialog.objectName = "renameVolumeDialog" + + frame = qt.QFrame() + + formLayout = qt.QFormLayout(frame) + formLayout.setLabelAlignment(qt.Qt.AlignRight) + + buttonLayout = qt.QHBoxLayout() + buttonLayout.setContentsMargins(6, 0, 6, 0) + buttonLayout.addStretch(1) + buttonLayout.addWidget(self.cloneButton) + buttonLayout.addWidget(self.renameButton) + + formLayout.addRow(buttonLayout) + formLayout.addWidget(self.segmentEditorWidget) + + self.layout.addWidget(frame) + self.layout.addStretch() + + self.saveConfigObserver = slicer.mrmlScene.AddObserver( + slicer.mrmlScene.StartSaveEvent, self.onSaveSceneStartConfig + ) + self.importConfigObserver = slicer.mrmlScene.AddObserver( + slicer.mrmlScene.EndImportEvent, self.onImportSceneEndConfig + ) + + self.configEffect() + self.initParameterNode() + + def onSaveSceneStartConfig(self, caller, event): + # GeoSlicer has a problem saving empty segmentations. + self.removeEmptySegmentations() + + def onImportSceneEndConfig(self, caller, event): + self.initParameterNode() + + def onSaveSceneStart(self, caller, event): + # Clear the image log view config to not save the current state of the view. + # Also prevents errors related to the temporary segmentation node. + if self.logic.imageLogDataLogic: + self.logic.imageLogDataLogic.configurationsNode.SetParameter("ImagLogViews", json.dumps([])) + self.logic.imageLogDataLogic.cleanUp() + + self.clearViews() + self.resetVars() + self.removeTempVariables() # GeoSlicer can't handle some temporary segmentations when saving a project. + + self.parameterNode.lastSourceNode = self.sourceVolumeComboBox.currentNode() + + def onSaveSceneEnd(self, caller, event): + self.initTempVariables() + self.sourceVolumeComboBox.setCurrentNode(self.parameterNode.lastSourceNode) + + def onImportSceneStart(self, caller, event): + self.resetVars() + self.removeTempVariables() + + def onImportSceneEnd(self, caller, event): + self.initTempVariables() + self.sourceVolumeComboBox.setCurrentNode(None) # Set to None first to trigger the node changed signal + self.sourceVolumeComboBox.setCurrentNode(self.parameterNode.lastSourceNode) + + def onSourceVolumeChanged(self, node): + self.resetVars() + + if node is not None: + if node not in self.parameterNode.segmentations or self.parameterNode.segmentations[node] is None: + self.parameterNode.segmentations[node] = self.createSegmentation(node) + + segmentation = self.parameterNode.segmentations[node] + + # Add the initial states of sagmentation and volume to history + initialSegment = self.arrayFromSegmentation(segmentation, node) + currentImage = slicer.util.arrayFromVolume(node) + + self._segmentationHistory.append(initialSegment) + self._volumeHistory.append(np.copy(currentImage)) + + self._tempSegmentation.SetReferenceImageGeometryParameterFromVolumeNode(node) + + self.showViews(segmentation, node) + + self.cloneButton.enabled = True + self.renameButton.enabled = True + else: + self.cloneButton.enabled = False + self.renameButton.enabled = False + self.clearViews() + + def onCloneClicked(self): + sourceNode = self.sourceVolumeComboBox.currentNode() + + if sourceNode is not None: + node = clone_volume(sourceNode, sourceNode.GetName() + "_Inpaint", as_temporary=False) + node.CopyReferences(sourceNode) + + self.moveNodeTo(node, sourceNode) + self.sourceVolumeComboBox.setCurrentNode(node) + + def onRenameClicked(self): + sourceNode = self.sourceVolumeComboBox.currentNode() + + if sourceNode is not None: + self.renameDialog.setOutputName(sourceNode.GetName()) + result = self.renameDialog.exec_() + + if bool(result) and sourceNode.GetName() != self.renameDialog.getOutputName(): + name = slicer.mrmlScene.GenerateUniqueName(self.renameDialog.getOutputName()) + sourceNode.SetName(name) + + self.clearViews() + + segmentation = self.parameterNode.segmentations[sourceNode] + if segmentation is not None: + segmentation.SetName(name + "_Segmentation") + self.showViews(segmentation, sourceNode) + + def onUndoClicked(self): + self._historyPointer = max(self._historyPointer - 1, 0) + self.redoOrUndo() + + def onRedoClicked(self): + self._historyPointer = min(self._historyPointer + 1, len(self._volumeHistory) - 1) + self.redoOrUndo() + + def redoOrUndo(self): + sourceNode = self.sourceVolumeComboBox.currentNode() + segmentation = self.parameterNode.segmentations[sourceNode] + + slicer.util.updateVolumeFromArray(sourceNode, self._volumeHistory[self._historyPointer]) + self.updateSegmentationFromArray(segmentation, self._segmentationHistory[self._historyPointer]) + + self.updateUndoRedoState() + + def updateUndoRedoState(self): + if self._historyPointer == 0: + self.undoButton.enabled = False + else: + self.undoButton.enabled = True + + if self._historyPointer == len(self._segmentationHistory) - 1 or len(self._segmentationHistory) == 0: + self.redoButton.enabled = False + else: + self.redoButton.enabled = True + + def initParameterNode(self): + self.parameterNode = self.logic.getParameterNode() + + def applyInpaint(self, node, event): + sourceNode = self.sourceVolumeComboBox.currentNode() + + currentSegment = self.arrayFromSegmentation(self._tempSegmentation, sourceNode) + currentImage = slicer.util.arrayFromVolume(sourceNode) + + # Check for valid segmentation. + segmentPos = np.where(currentSegment == 1) + if len(segmentPos[0]) == 0 or len(segmentPos[-1]) == 0: + return + + # Crop a small area of the original image for inpainting. This makes the patchmatch algorithm run faster. + cropInitZ = max(segmentPos[0].min() - self.inpaintArea, 0) + cropEndZ = min(segmentPos[0].max() + self.inpaintArea + 1, currentSegment.shape[0]) + cropInitX = max(segmentPos[-1].min() - self.inpaintArea, 0) + cropEndX = min(segmentPos[-1].max() + self.inpaintArea + 1, currentSegment.shape[-1]) + + # Run Inpainting + try: + patchmatch = PatchMatch(n_levels=3) + processedImage = patchmatch( + currentImage[cropInitZ:cropEndZ, ..., cropInitX:cropEndX], + currentSegment[cropInitZ:cropEndZ, ..., cropInitX:cropEndX], + ) + finally: + segLogic = slicer.modules.segmentations.logic() + if not segLogic.ClearSegment(self._tempSegmentation, ImageLogInpaintConst.SEGMENT_ID): + raise ("Clear segment failed.") + + # Update the image + currentImage[cropInitZ:cropEndZ, ..., cropInitX:cropEndX] = np.where( + currentSegment[cropInitZ:cropEndZ, ..., cropInitX:cropEndX] == 1, + processedImage, + currentImage[cropInitZ:cropEndZ, ..., cropInitX:cropEndX], + ) + slicer.util.updateVolumeFromArray(sourceNode, currentImage) + + # Update the segmentation linked to this node + lastSegment = self._segmentationHistory[self._historyPointer].copy() + lastSegment[segmentPos] = currentSegment[segmentPos] + self.updateSegmentationFromArray(self.parameterNode.segmentations[sourceNode], lastSegment) + + # Update history + self._historyPointer += 1 + self._segmentationHistory.insert(self._historyPointer, lastSegment) + self._volumeHistory.insert(self._historyPointer, currentImage) + + self.checkTempArraysSize() + self.updateUndoRedoState() + + def checkTempArraysSize(self): + if self._historyPointer == self._historySize: + self._segmentationHistory = self._segmentationHistory[1:] + self._volumeHistory = self._volumeHistory[1:] + self._historyPointer = self._historySize - 1 + + if self._historyPointer < len(self._segmentationHistory) - 1: + self._segmentationHistory = self._segmentationHistory[: self._historyPointer + 1] + self._volumeHistory = self._volumeHistory[: self._historyPointer + 1] + + def createSegmentation(self, node): + name = slicer.mrmlScene.GenerateUniqueName(node.GetName() + "_Segmentation") + + segmentation = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", name) + segmentation.CreateDefaultDisplayNodes() + segmentation.SetAttribute("ImageLogSegmentation", "True") + segmentation.GetSegmentation().AddEmptySegment(ImageLogInpaintConst.SEGMENT_ID, ImageLogInpaintConst.SEGMENT_ID) + segmentation.SetReferenceImageGeometryParameterFromVolumeNode(node) + + environment = getCurrentEnvironment() + + if environment is not None: + value = environment if not hasattr(environment, "value") else environment.value + segmentation.SetAttribute(NodeEnvironment.name(), value) + + self.moveNodeTo(segmentation, node) + + return segmentation + + def getSegmentIds(self, segmentationNode): + segmentIds = vtk.vtkStringArray() + segmentIds.InsertNextValue(ImageLogInpaintConst.SEGMENT_ID) + self._tempLabelMap.CopyContent(segmentationNode) + + return segmentIds + + def updateSegmentationFromArray(self, segmentationNode, array): + if segmentationNode is None or array is None: + raise RuntimeError("Invalid segmentation node or array") + + segmentIds = self.getSegmentIds(segmentationNode) + slicer.util.updateVolumeFromArray(self._tempLabelMap, array) + + segLogic = slicer.modules.segmentations.logic() + if not segLogic.ImportLabelmapToSegmentationNode(self._tempLabelMap, segmentationNode, segmentIds): + raise RuntimeError("Importing of segment failed.") + + def arrayFromSegmentation(self, segmentationNode, sourceNode): + if segmentationNode is None or sourceNode is None: + raise RuntimeError("Invalid segmentation node or reference node") + + segmentIds = self.getSegmentIds(segmentationNode) + + segLogic = slicer.modules.segmentations.logic() + if not segLogic.ExportSegmentsToLabelmapNode(segmentationNode, segmentIds, self._tempLabelMap, sourceNode): + raise RuntimeError("Export of segment failed.") + + return slicer.util.arrayFromVolume(self._tempLabelMap) + + def moveNodeTo(self, node, destinationNode): + nodeId = slicer.mrmlScene.GetSubjectHierarchyNode().GetItemByDataNode(destinationNode) + folder = slicer.mrmlScene.GetSubjectHierarchyNode().GetItemParent(nodeId) + + moveNodeTo(folder, node) + + def removeEmptySegmentations(self): + newSegmentations = {} + + for node, segmentation in self.parameterNode.segmentations.items(): + if segmentation is not None: + segmentMap = slicer.util.arrayFromSegmentBinaryLabelmap(segmentation, ImageLogInpaintConst.SEGMENT_ID) + + if segmentMap.max() == 0: + proportionNode = tryGetNode(segmentation.GetName() + "_Proportions") + + if proportionNode is not None: + slicer.mrmlScene.RemoveNode(proportionNode) + + segmentation.GetSegmentation().RemoveAllSegments() + slicer.mrmlScene.RemoveNode(segmentation) + elif node is not None: + newSegmentations[node] = segmentation + + self.parameterNode.segmentations = newSegmentations + + def configEffect(self): + scissorsEffect = self.segmentEditorWidget.effectByName("Scissors") + scissorsEffect.setOperation(2) + scissorsEffect.setShape(0) + scissorsEffect.setSliceCutMode(0) + scissorsEffect.optionsFrame().setEnabled(False) + + def showViews(self, segmentation, node): + if self.logic.imageLogDataLogic: + self.logic.imageLogDataLogic.addInpaintView(self._tempSegmentation, segmentation, node) + + def clearViews(self): + if self.logic.imageLogDataLogic: + for id in range(len(self.logic.imageLogDataLogic.imageLogViewList) - 1, -1, -1): + self.logic.imageLogDataLogic.removeView(id) + + def resetVars(self): + self._volumeHistory = [] + self._segmentationHistory = [] + self._historyPointer = 0 + self.updateUndoRedoState() + + def initObservers(self): + if len(self._observerTags) != 0: + self.removeObservers() + + self._observerTags[slicer.mrmlScene] = [ + slicer.mrmlScene.AddObserver(slicer.mrmlScene.StartSaveEvent, self.onSaveSceneStart), + slicer.mrmlScene.AddObserver(slicer.mrmlScene.EndSaveEvent, self.onSaveSceneEnd), + slicer.mrmlScene.AddObserver(slicer.mrmlScene.StartImportEvent, self.onImportSceneStart), + slicer.mrmlScene.AddObserver(slicer.mrmlScene.EndImportEvent, self.onImportSceneEnd), + ] + + def removeObservers(self): + for handler in self._observerTags: + if type(self._observerTags[handler]) == list: + for tag in self._observerTags[handler]: + handler.RemoveObserver(tag) + else: + handler.RemoveObserver(self._observerTags[handler]) + + self._observerTags = {} + + def initTempVariables(self): + self._tempSegmentation = createTemporaryVolumeNode( + slicer.vtkMRMLSegmentationNode, ImageLogInpaintConst.TEMP_SEGMENTATION_NAME, hidden=False, uniqueName=False + ) + self._tempLabelMap = createTemporaryVolumeNode( + slicer.vtkMRMLLabelMapVolumeNode, ImageLogInpaintConst.TEMP_LABEL_MAP_NAME, uniqueName=False + ) + + self._tempSegmentation.SetAttribute("ImageLogSegmentation", "True") + self._tempSegmentation.GetSegmentation().AddEmptySegment( + ImageLogInpaintConst.SEGMENT_ID, ImageLogInpaintConst.SEGMENT_ID + ) + + self.segmentationComboBox.setCurrentNode(self._tempSegmentation) + self.segmentEditorWidget.setCurrentSegmentID(ImageLogInpaintConst.SEGMENT_ID) + + self._observerTags[self._tempSegmentation] = self._tempSegmentation.AddObserver( + slicer.vtkSegmentation.SourceRepresentationModified, self.applyInpaint + ) + + def removeTempVariables(self): + if self._tempSegmentation in self._observerTags: + self._tempSegmentation.RemoveObserver(self._observerTags[self._tempSegmentation]) + del self._observerTags[self._tempSegmentation] + + tempProportionNode = tryGetNode(self._tempSegmentation.GetName() + "_Proportions") + if tempProportionNode is not None: + slicer.mrmlScene.RemoveNode(tempProportionNode) + + if self._tempSegmentation is not None: + self.segmentationComboBox.setCurrentNode(None) + self._tempSegmentation.GetSegmentation().RemoveAllSegments() + + if self._tempLabelMap.GetDisplayNode(): + slicer.mrmlScene.RemoveNode(self._tempLabelMap.GetDisplayNode().GetColorNode()) + + slicer.mrmlScene.RemoveNode(self._tempSegmentation) + slicer.mrmlScene.RemoveNode(self._tempLabelMap) + + self._tempLabelMap = None + self._tempSegmentation = None + + def enter(self): + super().enter() + + self.configEffect() + self.initObservers() + self.initTempVariables() + self.onSourceVolumeChanged(self.sourceVolumeComboBox.currentNode()) # Recreate views + + def exit(self): + self.removeObservers() + self.removeTempVariables() + self.resetVars() + self.clearViews() + + def cleanup(self): + super().cleanup() + self.customizedSegmentEditorWidget.self().cleanup() + slicer.mrmlScene.RemoveObserver(self.saveConfigObserver) + slicer.mrmlScene.RemoveObserver(self.importConfigObserver) + + +class ImageLogInpaintLogic(LTracePluginLogic): + def __init__(self): + LTracePluginLogic.__init__(self) + self.imageLogDataLogic = None + self.isSingletonParameterNode = True + self.moduleName = ImageLogInpaint.SETTING_KEY + + def getParameterNode(self): + return ImageLogInpaintParameterNode(super().getParameterNode()) + + def setImageLogDataLogic(self, imageLogDataLogic): + """ + Allows Image Log Inpaint to perform changes in the Image Log Data views. + """ + self.imageLogDataLogic = imageLogDataLogic diff --git a/src/modules/ImageLogInpaint/README.md b/src/modules/ImageLogInpaint/README.md new file mode 100644 index 0000000..ca9a437 --- /dev/null +++ b/src/modules/ImageLogInpaint/README.md @@ -0,0 +1,16 @@ +# Image Log Inpaint + +_GeoSlicer_ module to interactively inpaint image logs. + +## Usage + + +1. Select the input image log volume. + * To keep the original image unchanged, create a new volume by clicking **`Clone Volume`**. This will create another volume with the same content of the selected one. +1. Select the **`scissors`** effect. +1. There will be two views: + * **The first view** is where the user will draw areas for inpainting. + * **The second view** is only for preview. It will show all the drawn areas inpainted so far. + * Click the eye button to show/hide the image log. + * All the drawn areas will be saved in a segmentation volume. +1. Click the **`arrow back`** or **`arrow forward`** buttons to undo or redo an inpaint modification. \ No newline at end of file diff --git a/src/modules/ImageLogInpaint/Resources/Icons/ImageLogInpaint.png b/src/modules/ImageLogInpaint/Resources/Icons/ImageLogInpaint.png new file mode 100644 index 0000000000000000000000000000000000000000..b89476cabfe1010e9e717846494f5e10baf14aa4 GIT binary patch literal 22783 zcmV)WK(4=uP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TXbK~#8N?VSan z9mT!>XOCWuli*O?twoBIVg-sMIKd@okQ9p$2nm5Cgd|vi;_ehETC5b;;DO=}MPgUZ zJ+f#2pYLyW?>*u1-uu7Tg!H|&;T@d(VPr6RmE2sghQ0Ss4B(6YZepSplR|x`(d5m2brD^oP4mtT> zqTyAMg*rr@D?t5p1$ANIL=6d50@o<2J*VrcMyb5r>s*H8 zOw}n*XZiEIplX=;*9Z%>2H5<(iAaASLh2)=K1fF{$dJzRv!p&poTG&-wsZV0Atv3a z0pO`gKKGS5St9KzQf%j-pdt+sxG3Zy)l^iCo)~h<0&o0<764mLIVY&3QpS9^f+mFd zUhQXNhUba%oQI-3U?8^h0K~4V0f4D{5loMYTwA1JiyRkWJaMG+0s>0Ig{f*(1wqwJ zwHiJ4hXo6Qh1LY9)uB$?PYh3_t~~%C01(u{-3H7&UJvM0+1@e-FDLLoGT#xIxlTd3ZiP6Mz3H7!fdr#&4<iQ#!aG#z$@Z)W!vEFkaLy5P%p=gEJ6dM=EIpKv;d~q~QyU2@5L#QXT+7 z8V!)G1}OkwHoqPqNb(!GteLdbdNb*!^LO6u((_+`#l86LH?C4Hne9+el+NYcMtv7^ z$FH-VJMNx4kK|M<4ckKZkJz;c1YB51xk%cei1e4~3YPH!!n&|pec+^F(jp5aEOY?a zecYKrYc_)hNT<;NCjDrEd=#YW;6gp>TTgs$9 zcNMC%Uo{L}wW9<7zUmHKV=edao38@`01&x3TqQCP(AY8!9VtMN@c^h`0>Y+JIl6ts zX$yoA3#$_-?upF%N5vzQbo~Oe?{eM%LLl4Uxb|`@Tzbvx(_FT((dC+&TsoV>Rm{35 zCZ%;wzhxR5oUUDO#g#7Ay|F2gMoD}Jx{>NaS~pT{@5>Q`X+eaA5&&_F6IuO}(LRy) z1H2h%*z=bgUiQ%Y@4C9~J-904s!)*4xKuVf7wMe+2Gw=%?(X~hbhpp?zrKt|Bo3s^ zi}IGUspkMV>v<$VTwq~g1VGZ@m}INJ$a(=Wlk(WeI8M?DICPD!&2x|Ly>|LrWEYwAlSr7a zXGsGj&tG87S6CP|KoVpJ5N)-A*<>+A3^EY>^~=v)kV6@8RgeNBFgC~t2wWo`v9 zTym^yaVp6n?;iW|Gd*W+=0|!~pl3=02n5!XSWsah1pw}u4^js)MQKh72$?_cf5>HD z($5cIu1kTDA5jYa1uBVTD31DQS3!^+CQgMDHFg@q0P zYHWFb#Bsw53pHGYcb!vCDc35|}>M=v_+Q@V^!SLx&Cvh7L1Ikv2+ItEq-^CEZvKQ+1Uvm9IvrY*-C4#QYYd zsxSt|^=)W$J+a{q2^J%5uuL((M^(Me_vegsoWz!{`fItjS3d9h)oUog-WTWx#inv} zw?ztZv>?L&nvuYv!-glqR2$clL@-rMr&Dd&bh<5@$rUo`Trr)hFQwBB^Z3@YJu9)QB=rQT`?bq<;HR)ynXmcf#YWn|D!d4Nx9K*)kBmxS{b-4 zlK1T?aygigOGnjQKCI@@09njImUJ4Ekd`LM!b~(l1cR!3t+cWWs-cdN-5Lt3oUeL@ zCyXZ+2n-0^W14{UUMsD_qvOdTel9=-^n@t>D)fIE;J*_(C0%CJo6uTHfY6oh8tl>4 zdi|S1+S7U$(2+@JJJP8l>5Vdd2Z*O;47kB;^+wjOp za{>xHw@BJ%1yN?38b*v3;O_#Kz<+rSU`+RcSYn3{Wg^%q+1@DIU`{e$&yVbNr6Yd# z_fT8Yzestil1`xsMCoiHoyrw6>AZXbv;dj_pCA=9T{iH5a|S+gziR;m4u`JuO`u9* zO~)UABLGy|+gwAw-rcg@j_&X~ZaJ6-&X(Qh(iY-Yj@yX~{%xQu(F>{M!fJ>QP+d?O z;9nX5vAXwC@6~;+N=FxWO26#AbaR}xH+$X>uwmY0^vw)!APC{#2Vp7|z~}(Rr!fJU z4uAjv@}+dD4s*~@4uVFmX}ssqBhMc3{8R2>jSi|?Cttw4{Q&!qS%>-bA>39=E#oF^ zvbo#&+RF#=Apa`ZGhXuo+9|xf4#oj20s_qm2nB%qP99EX3nu(a0syB@@+Q-rjk}dnSw7a`MFN*y+=(-XyJS0D#vTw7+^`K_vZ)w;xro?Il0}08;n@nkbTi za^-Zo4ov`QeqXAd>+Ab(xbetKUwzU2Vv&9>!+jw@SfsArt<}4a+i}_D+!43k^arxF z$siQA6_6;_Jb-o>j^sO=6_kpqFyEDNFvDCms@^?x#Qy;x37+||RR6l9J!XC^;hqv^ zWAknLwvKZ!TQ74sw)6TOGLv8cfT%IG4&=)tz^POfNsmgIR0QJ@&y>1~G7VX@Kn{S^ zVJrJu2 z!aSq_Kz7V7yI;B2jaR!tn{DC#`s%b})NmBCDgeN2NwjP__8lcsMdBs(9uOw^q(vtE zvHGuMxtFeBHCv6MN#o95AdE1nSdhWIe9wL{Kk3>@VrAn|=VBzxrRxSY!u%$VyTR~o z#67sSC)5k-?J4SW^ifAl-D;~XC$F%=^5br}@h10|haYuszWJ8>_Sjn8CPFd z=bGr>H#IgAH@bD!T8l11l!aL-7!<%Lt=@sT)e7fJ_xY(e` zL07U=SE-hvu)Z8t8!BPcK-aLb90qv!sisPpZiE`jVVd7l^%W?rrt+0)3Likv3Tj3& zf7Y--0pPoMrs}=a%jPGZePq&}NVxj(O-9ba_@Fzt!T6w)@u#0Y^3q)f^gnNeZ~r;nWS;!7{PH{N*D$+nB(xm?Z|OfQei6ck7F-u$?WhHLS?V1CuL zx3{|`ms-+Id++Zj;Ptm-0y>~lF_o&|7>4)&ZsH+_-MQ7DZ+BZPvz+_>)mQh@+A!3- z295M0$z`HyBsGpY4*^D*b7tqUVI_eF!b$YY#Q>BBg^CnMj$=GLEN3#n8W}U93FF88 zY)n~j8o+phy_eMcKD=kXY`oUtMUl&}gmd{OV+ciVN!l~-Nko_+Rt zV`z^aJzPD`Tqct>DaVH+7A0xLsTdq7L_K91V7cX%Q-RR@)*dlxJTo|`K85s0n2xO$ zUqaF^yKSp_av%yP&gJ!Hu@Yv6?lthv@y8zjXFU6Qeyiu&Mu5_giGl{jJfI#B)FJhG zDxL!reyn2dUJ9CEL|lgX9~KAzO!`gTo3kgP0kHYz#oJ=cSmI6PH`6R)JoY|<@lj74 zy9-xeZMAU^Kk}HXR4OpOJCZM{hp926#dvyiMq;>UJdcE=83IV$&1Q3Mc~m=TYy2+J z;}C^GB6=Jr61Czs?YEedZ-DBKG;FLFNT~BPc03E^9(v?acm73F-NAe8@h7fpg6T~# zy*Y@g&7i!ACpJQ=S3S8)y9w-moWk0`+QT0FbA_KB0A}axp9XgXf>`Z)58pd{QuRS# zt8*Z4^CLNYLC*mL227G;_YDr6XMElm>lxl9?Z$M{#&k6)0&RrEBEV)2%Jc$ylEyC-&*|R6W>FK&PH`%!U;!7;~q&qESd@GhST}hThra8f4su|VaV~QxH@KyR0YF7U)QTwFT1xJBoOGFn7`^Ot4QK2 zFdT(0LOF=}9t7^uFTb$jZoRmA2~j|oP()9mnjy|XdAL^p$tRz>oK^|R#83ulrX+X@EcjIX_~4(EX$nPdM-T8*X$nXU;-un>(exi|IkkcyCn{8tLEn>(|fqfYAbo z)P4nDp_Vidl;yX6i}b<%2W1gJ5C{N(KmZ%a@2_8f;d0I0+f;J^#0zd+E9I0iwD7BGJxHU)m2LM$lr zmH$5v0Oop+pQ!$>@qF2S-`Dr5UkxKQY0SUrmRnp$M~4m8h|zOPy!Yt4m>tK@;S@`sN$g+SY2_PY(d~LcEre`H@+;|NaNvz-WTOX|f`NqjTXZ;Xf8A~D}q@W#hQ^6NnQ=d`xEMSAykFuNG0Qsqi26++c= zN^b}Q$2s@3M*rykZN0=&Ji%6wm|^g-4d7(Z02!%&%?vX2Pq|b^Pu@4lX0xuLvB5p~ z;6v`9gASZTX0b0oeS>67BL;!J*^GMy_DHpvujBj(^Bn;3gbq)tkl8p;?&s^xE&Vpq z7c`GObMytb-*KmFYj2;6`JUwC0OF`j-jMAd^qA~_zRom&J2e{H3W+1gX)*|;c>o!% z$-Ml-cdn?Z-FW|13V?HH`~1NHUkG20;Iq%aaP2TvHniKG7uoJ zJ%s=PD#GCBqK`lMguij22*QQRDG;d4D!VrezCQX*;C|_)m)*`g?Kpv31Og!#9|&Qv zcAJp!vZG|%}z-vQ9&{ad%MR{Hhs-rnB}0HUTx9(l~#vR38$M)cB^1gU*B zenL`_%(uUB z8LhZ+`4bR$t)RNe6v)ZF_ul(-nV2svpx%(iL)1PZ?+p6cObH0)X_#*Sh(n?QemvwW z^$*g$2^f9G=~JG6;YFlE1A$$t(>I*Qk%CFSn2)qoAUS#^x^+_dTWtOtclxBs7b3lS zrCWgcMX&BN#&{U+8BZr2^`95xF$pj})xfol+}DK0fKtuR{_s6+SBUyw+~E{za%i64 za{@pKR6?3fOjj|eKJv;-BYyqsjbNrW0#lDf4yb&!S`c{Z)BYkePuv7i*At7Um}aR8X%0JjGqVPUlZ1Kozck`Q=yK?Adc%K3CVt z{4Nl1g@su1VZ2`bs&(7j3$9o1UhYp1K6v>FW6nGurnF|4P09r4!p{1@M7nL3 zFVdZg87B3exof{eM*Q{rZ{4%AesCY4f@f6dF_52_)6BD%sjGL-&HTYFlFhmH58gj7 zWpn%JBvdu^t6HiHtL6G~Mbo|lC{te!b94>s=_WRAx#5OG|Nh>4t{YCQ28EI?VPXap z%vYc;vu4e5vmqU~(Azj>@L+e@WtSbpg+Fk?3@%l-a1NwyVLR=hy@%B;RF^QMOE^y$ zH~%z%>2J$lAc*aMP;37+=u!9f^yx_dYm+8udNPmR%2YC4z?0*hZP&avxfv2zftMx~0^4t?{_&aa7d;jsZd#BLq zzQvDFOV1+T&j8_5GiRXo)9xSi+ZS7H^&um--Ela;K?|6FASY4IqIwHyG(x~Rtt|*E zfvY_J{PXAQB~w8@0cGye01Aq21j*NvBfYK`(Dkn3*=L{OOzZ=AM^L6R)&S}fCVqf! zop-*#`~m>BpF(=_Ah3#b&g}IjAaHehZJN~-F5w08AV})x8_hSS#_zorPy_#FvrXOT z$&;s|f;&*frD8fL7t%;R9zJS500bF4|J;2CA3kcG^*0=`=gZHy$C3O{jgERx*EhNp z8b^EiPynMe>2y8C%HY+*n0K)2C%pZdTXfacPPumXfupoisGJV!4FrfcAV>iMjV4wB zL8S^Fw%=xJ1F^~)8fbmllh9c=BEP*1J%YwY_s%=-x?#gkkuM-8(3=1+EU^iwoi#Mi z_&>h@K!qj$%3rPF9YB$~fSr!u@y8uEUNd#;=*O$8+r|7Y4WPD5q)7*bj)Gh67r$^< z-hA_=p7}ws1oMlTOo;TOuhRkQKSMe3v~Gcoqae)N&OxG~2lcKp<6iIAw366u%v*I2%;*<|C5<(0>MTbZea5h^$sE_y9a0$T!kI8Ri23tZe#|6qr`*90Jyi7cJ(@4I6s$_)p?tR9)?< zOZDrAgz?@1l=JJ{I%}<|q1looO%jLnRq^_-FXp@QWzvCti*yB`8K~B93d;;|}G-7C4fbvz= zSamfU^0l{koOVvL9m^SHTe%xl(e%6LD0F35Im=6F@;jE&H ze;Nc-?z7K6w>kOpfXxILBj@K2+C{qV3SUVO==a<=HNS5W`5y%8E9zz;}s zoIPdw>uwH`mqGGtnV(3tF_}1#_@5&Ba0Up)IyT@at@}AIaGgX?;+ew+k3{vy;{rA~ z6lrj%3IK+lb=D=zH2w zb2Zq1J1Dbx98^gwT;c~%_?Wc7Q%)fMz^9*mYL`^#hM8p4sD7PSKAlp-e@W7E%l!iB zcV#3@-L4QH0JUq9mm51-a0A!Z?X7O=Cm(3;Ko-Uee40a`$t0m>7FDU*iYu-_P4t#B^mURBZhP_omf(Aq;$+wD++mA zni$gDKRr>gol{vvYHzOdCW5qq2oP{y>jba);uH7OVMh|Z!^9}cCykEG>;f2rgKokY!8H(>0)H^%=mYIKE9_01PKX5x?dxf-xb>3L;v_1 zraJ(b)+cDp1jq+K1C&o1J!a}6eHJl^S6u}3B95#^Gn4uyLG{$=q0FM%hLA8`#KVnL z*d*?X{)wQ>=2`Gvz*zdNMLilIT1~dg1~by9ju?Kn#?2EixK#Arr6l;Y8c1nuW+k*K z-6xMZ^J1jmPudIs06D|#fOJaCFKeSF^3McRYOU0Z`DFMSyVvfZBSiEd-WX zvkU+{WG=Fj*U&r(%6J}zq%u~%q@Z!DDfX2S%GF0sM*rjoXy&hJfSLwTPy^|;Zw~;# z1tE;Ig&Uf-O+%|kpiQ4rFU_36)4AWITaWZH2_wrpmi#2l{Um?fdCu-jVo{uzfJNFs zhFN-}Wwz3398+l`l_w&t>E$-nT<IO}XeE zIB?KO+FDv*6aeU=%1p^uDXH9gm>ApE+G5&FK8>wT{s|C>G%BdF`&Lk>Ev@~a!uX?q zGR$KD%vC5AGA?XP9tc4>9wF0#f?_#Gh4zAPXsfp?%g|rzpPh0A7&sHPEiDAuY$5q4rUh^W zHO#B~Zrh54WXjLH8|prcIPsAyNC zcWD7Nj4{$eS0(lK&o_i;zJ(YHFCI2{Xi0hMRLm})_1s1A_jTQ0%XEbwA9n_whTw0U zP)gc>L&{J>QvnFg7qpKEqYbX;e0~~Y^ugz4my`BKKOH1(wc+0!$FYCr-1!TOrUd|NmYB0LML2{S45F73n}LRg zMgSeyFt425WGY)35QzC61Ufet^T{nh5&(diJL*q0F)f;ek}73VsrGCpJv(P}8dH0W zJ2SmIM4TcP1$&Ge3uz;>|2WJC6X2yZ4ocJ#f1^l!`Z#H^?o-;l;8NMOx3yZDlku<4 zcNKMjw8@){pTxf?+TgN_#&^qVj7geB7x`ef*D%$$hQBunNg2Az(iOU{vy6O_V9(ZH z?#ZRnu6rgYXxhtfI41uD0Kiwef~Q_^-YG{8I#e@&UH2Z{-E15@y(u&xpba#@l>ot$ z);VabjGv_}l_$UoQHwBN?;SFQbUM?X)$8s|22GGHrBWID%}-jTOG>WRuJB{#cea+!+xvW_?L_D8W|jG^Z#L)p)@g;e zF7vh@>91(08WT_g1jSP)Pde$rC!aiZ*eNHwefQbNEwSVhZVnEiw4JGb>Z6{J9Akl^ zp}tPqMcv3MOtg_sF;~p*NT;%T)!mU!=k!7fM(225OsOofO<@Os^h^MQPp1HaX$D{V zt}vefp!TpP^6k>qga&~6`UV>XRONl+tC3^XXCZLfY(bKt5kj$jc8hJkfP#&*I@er5 z;1Q*0J<`=*>f3&(Ptf6+ZzP~7w}grDd3%DVABVY^pM*HC&ij?Go&&R1X=-xoU2*XR z6p}8gq!x< z?xw*djdW_T%SJlI{Gwhy`TdM->N?J5T~%K^Rh$a)v`(HFAlTPXGXN-F(4xUI>!zAE z*z@czAk0Sq7||Za0<~V^)KnWA8}RDuI|0z80TiSKe7a@MY_6%;gb6JVr_pU}FU=(x z+FyJ5OOK?ZDBQeHUmLeqp2jTMQb{jNl14}vXgSwBPk(F4Ld*!=Z};d}6dzl8?YQc8 z>bt0}wB=iz(l~|o7ytk%v_QICNo(P0P@?fWU}`(e5w%@@(@i5k`0~rsjz9KTx88c| znS(fMwv8#cp1pe6fRbmvsr@V@iPx00dYE4iI2w`eMxNC`&GfnYdTL$==`*<6*)#`C z?X&e=K=1?@0KpimE_I_K|HZJ+SwavELE1~0b2pAWJv#cdk(8pEhA~<-YjghiNvnRE zAuWs6+COdUw#br8E;;_Q&pxyDe;QWxlR^E+pn^uuqyeNE>d+WzG{!!A?deXNGG!8K zu)UDY6k9Xtur-&_tlw;VCR4xI+G`JgWzH;{>ZJ)FHpzoHdCjX~Tz%b3R>Xp=iBI`J zTXcdz;L)*wq!=K2`Nqd5osz^Bi*C)vW_Qf8%ex)=E$%kG^nw#}VYMS4hQ&H`dLH*5 zayh>heHx%@QwdLU4b8e+VWOc&9X0tck38z$cM0Lzi8k^_vkcM&;HmYLX@l5?wFM@tyQ#{Fv{~V?TDovvYKN6XgYYG z0R#GjM(cZ1@{npfdHtcipWYf9M=0E8EcI4q9wUci^gPxD&3tY;W5?kZTYK03Z-hMM&Fcp6SjNDSHuc2?*MoPA1_tq-xcU@J9`rYmStD0< zIpJ~dvh$q+yG1H2jVqhuOElasW`CL6oQUwGj{8VD_rD&PyC1u|7A zu=Rd8O2=)t!^q)pzixB*G_g}-_98O{8iWFv_M*rH58^rcr`Xa^U}ga3#~=`B^iipD zdz;&?-xBV{X)jHnUhPPCAyrm6zT0kKVU>thd5SZqP3` zaKB$^6?fn{LI{q@(M^z_qDclrSWK>`4820Sw~Ql@^gCRV7cG~~Er-RYAjPsA7SOTM$o z`oB!tuk=mT?YqU+r%w6c9T%WEi37TGE;n}XeaBw(^xxdmpMHYl*+Fq>8w0G0 zN|~wO@{9C!hitHsyYg?3AHn%Hssu$^um~99Zx_WMtdLoN7Xm>!y~@^GN)-qIrt(2z z8jfl^Tun>zixW`y92;@2Gq&HjhU@j|b$M{QPuxSsGyv)y9?2gM)ke(9Gixz-2(v}VN zE<-n3eon>~vZURNmKOKeo3FbSmt1D2>kc?{*V|rx@j+^+V9iIGrc$lt0ujp@q#=S` z#+@mfo@Z;Wqd<2;SP56*DXu<0qbjZQ(q8qNCfGKFh z@91dDq}3zPrVrZiLDRLhqPPCyUH2R@Wcr)#t3oRsQ+lPqQ8RC-n}NZ!Fn=ZZKu1Z7 z1rCitxKhdGDbHyutm-a!${0J-*_ZtvM-Y$jCGWx?pJH9IsDF-UvQ~r z-&IB{m{?IlYjm+w8bH(P1YT*X;;fl&DYU`ehaBN{zvRLLxw3`3TDZ_YT1Bj{n)vS+ zM6Q%xWt**}W{`TAZ43``p3fXli?Sy&Qa}RZp_#^j+I7y00qD6IsC4=DJUap z0PVPc*=f zwLsDxG%Y|{3rbgBd1d$ZXP=HOrPHm2bkKoASZ>cq3#6TvhoJ>>75o8Is_Iz3`0A^l z_%^EYcl{Q1Z@0F%uQX*_Ocwyug3uzEqtzRm-8Oyty5IL(%x!<|6=!49%d+nz z>1!Vp2=#9&A7<01_G+IxNsU0}Mj9A7uvjLFa>Nd|Ue>tGiTfRJ<8{wJ;~selNe6sO zEw_T(cJ(#fEocgTizb~#7R6*^I6#mFFrg6H6VQ-h=l2Xl@=#Z`FyjX|Z2O(uZO=V( z1lP?1Ds#BlK4L|M+sUawFR3HgdHmQWvprtr=ZN#3;ZFP2)bk*9BP97I@gsH|aMe>E zzVANe0osyfk>+N%{!+`h?N(jg?RnWn`;j>+Ca|w$(J0tR4+4F)8FgF0IP+5B? z;43_FAmF%;t_cU`k&#Ya<8ytWB`<`DHQNw)u<4{?OF zi|Kxwcw){;&5QBDpaHv_qYWxAy6)Oj&?KC-cOGIqx-ZhyFwGCN6+n#muK$+CehsdYJgvGg1Bo1o@>H`(l({ja&)eMJ8= z->sX=_v+>H-Md3QTwRZzF5k0x&;$5fU^x}wH+;0Xed`Y8q1XuWMB1$qNZ}_ z8p`D~zhxULp*}oa$36Aj-?;O}o1Syo6<4|ore5q;U25rbj@)W zv9w;6a~dK~p;a_29DC9p;CKDu6zpJ>`j$2*6^owP@yMlO+YF0y(gNQ8don=Oob%Vl zX&Oo61_b$M@Yi~2a`g3RNicUR;YWx>vi)-YlI=y=7j@0$(;~sqFWw+7Ft6Tvu1fY6p6p*ixM5#QfKASATd0-mYzy;L4?Y6&j zef##cd2?dA2Z5g~i+RGtQWZW2G;LCFaupyAk^Y2`o5|e2G#R-|4skE-uy~LP~Pw8Qi zk7+yI;ct`;k#}eyjL>*SzTa>Cd-e6B`nv}Yx%#w+?ssWC^jw~wq?WNup7JguLS<{I zl)O_-na-jeZhYZ6H+q*nt}+0SANEfGkiC=I!$`_a!D$R^B-+4(L7{~GUTo1t-HIz{2^QkR4PrFoZd+QCJP;k4ixU$~N z`L}g=^hkXXrj{BiChai3Qib`(c%(huSgGg}^SSyks>@YElXy%()BgPj44XK4vMuNC zjhC*S%ChOK>qd7?+rxbE`4{f`>#uhc&Y9?bztxuG$Br0r9!y6AL>a`;US$IaYjirK zRk$HfRA!@E(1ckMM*Q(LRq!eCkGfhnRqx(Pf=Bus_M^D438KhpXWwq)y1-kTV)l?Gbv`6p<9=j@) z_yGs2rKRN|4($a5XDg~G6~|k4@|aCxg;j1Oiqdbq@#a$-Y_P%a-h1yo>lP-AR!~}$ zXau#628A~M^{?G!H{5V4JJ+@@c211W^VNKGtcmR>|iGZ%$m06m}=J?as- z6lJ!q_`1t2zx6X8e&EvFuh|cs?JEYyl5M0m@eN}nKS0i)-d`w|Z&tyXT&7na?PUG{ zS87POgIm>8D?w#)6LCtl@ah5H*wQW229JOE9%>%Rr(%g+@#GznWVbUqpu+ObWKC`OzbmnXaH^a zl;${#inmRlxK66Rapd4*P9Ct;*2gc~uiw$v-EgBV-qd1`dQ`q7KZigh+ruZl7uy($ zf(QiNyEWT%cWoDc#^^C_&E=NA`lN#oxskG&!wK12JcLA|T3V)OZ{IzBe;BdI|;o#X=HMwm1FsGTHzi4?|m0?qKiaVK8#>CBmC z?`1NCL?O^SAtE8(qtjw4?z?ROfCgy;eV_RyPJSd&vr@<##@qgSAl{roK+r)+%nTR> zuH~SE4zl54Z38F9Yq?Y{5TpsTmVTLKmT{9WzW5CCmbV>puK>WIs_8xZJLpzv1aRk- zS4I6t8nlT}=gvkb!w~mdudb`v@q>?@c=}<7k2`*!{l@IQ-S#6lU3;BVmhId3_=_%@ z>K=ONA@}`v-@ERpcS(IFC6(`)-Np1SQhQiTUBxFksse(5`@jPa*bHIHrVmn3n1`10R-nZpo>m1(7;-`rnv!r z5J)j3^Fxp?;z7_Em&mMxu7&QlGdgL?!hVj^+-_>mW5~`KD9{)jYZ3=7AdRA4hE!bF z>KOxoD?{EH41#uUkrrs3G-=ZDha7USF}{dLY!Xjeq_hTtv|qq_>#Zvomngp0TUUrR zZG+0$Xje4p@T2Z}_M;EnI?MjTjk@Z}-7?$<2yjaDwJ%foX;gf6WobCcrUIG9m{eGRZ?w^4~X)&H| za!1i-TWyKoLlKfK<^X|B6cG?i(TH;bDgTYE;bc(;Og(RKqtt(*PV5I&hJ=pnzmLH= z71nvBbsaH{(^GTeWl~^1iC6;|6s;(|%P+s|if9ylPPQ2dmx>oPH8j|Uj(*QSziqwZT1ihRVR#a&t&*T~8oAtY zhaKGCo_S_3m@eCEADpI`9U^X!jg}^CJ$$20)II>dd4#+{0Mdr4r)UXr(@j__1-jLP}Pu#mDl> zWpBLgA?BRFkN{iz;Sn#>ORxQ{{y4kd#7GNx5TKW&1vGsc=S`-79NNiA8zTY&H|N${ zZ#!E1>dO>}O!}!{E6(4_E3f26Pnd9m-6vl{8UP}1fxxhNJKlfLqo><`rBzTr=6T1T zdW$mVl}Ay2(x@}ee)FxjofdGU(OkxCOqocGt(Ejz#;Z~ZvlSAC0j?NP=X)XXWVV}l z)~wlXyY04fuf6{I0bC&m%&UE``t=pjg!yI)|8VnlM-19%V^`5cSn{Zd$uZL_0OH4K z8~iEhT*W4=bj+FUmRo#Dcke4N9YX=^Rj_|fRUi-mO}6qmQ)4lnTX(Hz54#m%oyj0~mY=dH-IM$n!S$Ky z>-gaZI06qO{bD$buBfXF=~y5T^CQ|^`m`{6mh0Bo?5^5tUuoY~oiD_(uPBE%o^Z!38QJdmKdnE7R%*D>PSOg& zqk@D|rOVi!YDdAHc+Z_~{J{Os-fhY`nyN(IOm@5mpqC*K@X(ZPtpaN*=@dP2GY8mo zS3nye-L93UmL`}p_2NtVpLynJxAxj=xwY0>%bk428GCxLnCXxPP~Ttnzk1b4L+`m~ z`gGU5Z$Gy=4i$_x8)Q>OxPn4xreb>3$tR!w-uv(SmB*&0x|p9ZJmGsS(`y+oCMV>N zL-zwx+Y9sehd&H)ci(-_G2AZa*51OYT}f+~2u;uOOwqBOv)`owtV>Kh`}L1M95i+B z1Kdh|`neK3D9)VWI_RAhXU%dQBBf`~aeW(`+_3F-bcH!{jymwFKOVrnb0EzFP=4`@ zATv1z00`y>VS1%)w$dwK*=EiO>L#fodNDz8|C_HpaOuGP2R!iZbk~l0K~$u}&7hMZ zS)l4beW>+rAoHXxw{fd2wuF29r5D`IuTOI)|94{l$j>cV}E<%ruK6A=D|lCzVE|NJ$1dhl4uRhkyp3U-n5aq>#^kG z``q^Q-`w6CY~-%J{k8#;`i!I>rlSe4{Z1cz%=Kt$-s;uYUi055sA2xx0PyioG2fGz z?^S+lD-5FhyXmHzxGniYo+7*gq*3{EM(o-^9rnO}L0Qm0#w|8!EMv z)!$X0`{4b1!%FF)O9t-0)1vq^z3GlFO3!4~ev7%SR$0v*wdrr%$ZfWFr~c`-Lug+C zCz1Rv=9^Flp81hY54UlMV5bRZb)(U8Jk8z%(E2tS2;Lhw)=-zd4ZP{FqputH*k9a3 zc-9>-+W2s=cQ?MP-FOb>KUp}pHX(&h{D=L5}u~{3Vz-~Dw(Ye_x&tpc{7Ujo;_$> zZrOVu!U4SR=;PhQtFP)W$4_2AagI#tGEr4u^KM+CcOUymMHAAKc=z%FYC#|p^A*J8 zS|BJb5XeDQPi5I9K%%p8cbAV9$ zN*-RgdLC0pF*Fs=##A8ntY{Vs^510B6YZ@wmWGjbiJiYr$&UeGV-OmZH2^fyXd0-| z+aeyWNg!4Ldm+R_8x&J`rR#4Pbkx-^fBvca7<;32?~8To?$%ynNq6k^SDj3g`C(r@ z0LwIfp98Kq^tvnE=YRjZ+jxTw-DL+H>JGbT$`)Ls!paktMzHIVN;*WE+8 z+%vn2WY-hYcn5wCmK(`0!Tb*TBkg{RTWvCx!7OT0XpZ)mW{&4y+QghL=Bqs@9!w^f zCXJnUKbk0SpL1e*V)HvCpPSU)>i+4aSs3g*;Vi8ui)WJ@J3yGQls#uQ-KKX>}x9qa+Y8;etH{b9ZZV?Eq zFvPk?h4L>*qN*GP0YEIUXHW*c%G9J4n=Q*tifzps=ghI>_h6^Ze|z`vKTdTc4m{L7 z`TXA}P~ z9%HHp67#JvkfD$Vi1^PR8i<6U!cNKana67I-EAMLl|L6{M6_nWY)bhf!Q2Fdjib&O?dHsBarwOe+^Zk#=^ETokhFWI|Dp*_Hd+7;U~2(r0x80T z*|wM?PK%H$y3^h^acJ!&e+f3iXw$-xKp>4^1P3}9Pb}fo@~{ZOh%_ZkDn|{7nl%uZ z))eB%ga>Ml0BI1(u|T0hBzD=5$OBDY!~q^4=J^teiCv6z2X4Fl6Ng`YxeM`R58iQr z`}m^|0f8UA<9Ag;l~Jv%W^oYn1yElmlMI0nO+Z6&+*ytZ2OWCTR%eZJ8?U~WtIV9a z2WBLcn!^O>4It>zYN9B!+g4lOe*Og)xbEG$sg%CI9}oP9{JD$`12;7_S+`U6eT5ZP zu=fumN1i4d-{rk~#@EKlZ;HKI_L6C;ng`gjF8~m=Wm8(*3IH;CRj%(6nnt6Js+YBO zO>DwI8|im~wJk|nKGvk1tP!dtjdn2YY}ZLk+iUYXhmDMa0pre$Rj57GKiF#nv;+_K zT0sR!ViiOHu);u|`HHP5hko0_&8zUcuQ14|@85%Pw{wy#1EjaKnw~cQyuPip`IKK!B9=TUn@C@SAmidF6$7+~TGjag4j+;fDrtQyIyx zK$;ieH36pJ`27#K=iPVTwIO9mws1@dmOamaki6#?KFVk+^BQgM}Ejgz)R+~y-Vi1@B z$w4ub0NF7YYXPXvnPyh3#=2wpNZsRt-OfH!?*?s_s(-kCj~m^qS00R%k>>hX8a)bi zKaaM&tyIL4A88q4t}!p(3eQDa%5Rmr?!=+b9(CPS?*7LfalMeJs|OwFCf#xC7Rt)9 zp&XPxj&TdTc1lc=KBS=7K&dkMazw$bRO*87TjsduPZ{YBx^T(?Ah-yX0DxxnhS@MQ zO^~x`S;=y38xT@{wuiCGsy0%V1RiNCZ^gJ;X4V3MIQgEBrIX)drW=`LLY_&GI8mYm zt*HMwIRJq9fd^WWN0Y@erQWvMPbt;Y_@}gfr)d7b5@H+?c}x3tGM^i`$!p}qG`m|z zoaS#P=$zBmA!5E7JwYJP5Kz<*x<`c&^DX94RuGDzwB^HtbfZcplC&GMMf|QtrJ=nR z(K+Juuh(7g>G5OVQMyyg9 z07y#mRoF!81pqN!4IKLwu`hu=KWvAco;>WDD_rr1@7=&{fA2p2{IgwYCF_--E9LMB zG}bNR*;4=cD-S*5%&+FmafNcp<&d1!7hTMq`N%`=p68!&*Bv(4oq5MCr%>T`D%TE^ z+W|oVEub}gfIxHl1p=G_fWYr6x2@M#+0|C63Neqh<`B~o0Ms%UW?T2`M@% zp1XoB+!0q?dW5b6l-2{eZSe7D-~8$;?t#C*<37TP)Smm|mNI8~*x+i4XkQrksuJznlB6z1>~?%#$wF*a+YAbK+BSD;h2 z-qzjv)Dy!f#B2&MJE%s0AZmr-fS_6c0Hs{Tuj)k;Xx|)jlspKMU`(uKY)qP8_ixVY zMiFVt1IACN&Fo(?{I>z{Q(^D3F(1kkGA;=K@&UXyNI+m?-7vcj#Z^x%=4-dBCeCXy zN%M(2?=p4h{r9+*wl+TnT`z0stLtU1O+2+e23hI}l;2NGsoH!62Wgf}Y4#j<*3P@Q zD;|A#2$j^Tm|3Y#5LDZd{sPSRAV3oc1oAX%0H6e9AW%&46emo!-{QFQyv||c|MeN4 zgv5-@lOTr!o~ZI_7(~+IWSCN~n-ja~9jHJ~lx7-;g=*;_xNFGCQ&HcijoEHTTX>@N z@V5FTow21-X>B4AFG{iXav6V4$C{z+H-zGRQ=>ceq5It>2M#(F|4(AR!aUUg zwIR_4Bn7fK_U{{DNIpdJiR)o{6Vl%-$)`1YfPo&gX3s^JSaQ_MUwr0rO-*Jn`-vFSn_gQiz5tACh} zA7GOt=^@y3siG#W6x}0q4G+Ke%Ka$P%-9^@7^;_`#jBC6`LC~3D&0z92oMq`L&p67 z6^#D(!h8jQf`P#M0|JBIIs+c(F3;; z)tpF9X&{=i+cxNWuo zK;_%e2HMNO_V>XbsNOVk)c;y$|5Wg;I3L3;r_!Ks3Gt6@B?>zTY|VT^0D-t`Lg%~R z{)3X?KMFS@se9=VNxi9m$j*Bpc#RPMu1oDWCem+z^OY2ZdL!!ctIq17aHaIT+!hO_ zrxv!L#+dIxAVA7Y-H5@E#=?z21pjRsl#ihqZLL4*<`Im+P?Cc;-t3_%U|UNAc^ zrpB?6()PR0)pCwOBfd+-^;@roMhQ9>#iVg6t}sCIUwTCDoBiGR@NQ`Lz~XTu)S=-S^w$_v3;H ztWUVU@Nu8;vjPD5^pPh?qL`mBCYjGx+-$iOY-dkRH@6{P_3cGsx`;l$VgtxeoU?m~ zwU|^ZgEwDcMY2`inljb-Jk6BxW$`M-Yk`F<%=d2KkHaqG$Lf1F`mRkpBy1Z}B>NF{ zhkC$?-raiiyzGrHKX-M_-JHJG?RO#3P_Vjz$yk6`C{q9cB=G_XOt*z9fTv^TEVs?7 ztGoBV`1~*~oq?>+g2}VFsRiWgb+LBJDyFN}4GixXewu)>P=y6y0=(3D)vu{bG`bD1 zNe z-2wBJuSn0bVcb|7s2oX|7P|1W10YtjdS@&rOpUKut5VHkjAqqnQA6RhyYCou)Ni(Q zm07c36KY+Ig~7)3QWqFRewD1!fW=GzAZclFV}AF0cjWa~4}XQD(A2#(V+D$^a?+p9c7Og0-`$ z+?W9{g6^M5rq-_6vS}@8vPF{5ygPmH=>EI8I&8jH$yVCiZH(Wf-K5@+`P=GPe(#Rc zcfnr!xj(=B;$YoRUdpEsn*cv=&r3gO;`v?^K?^1Se|`XHmZw}d8r^FE-zAeo3IvkQ zWKCN;JkbKjmTR8;>)^+ZIMyA#`BrY(p1r(p(AMV4sQGehoB0Gff9w|9xW|q-#!Y$X z{=wXUzi_g z>rsKAP3JhaCDI-U+9FMhB%SvEt=<{WlV8}v0s+9T0H6}2Gz(e#1n32g<_snvNY*22 zQL2Do+tZ8r{=6+-wM8ME(`zC#Baoa(sd*2ANFXq#%LkAa&?Jvo3uw&2FOuZpVYV7Y zxBnjie%{dkoU?7OAh<`T9w2xS{& zz4icsF&i@y$dq($kHUFv%#@e`Ek+GB7x1oOBL$*`GSD6iDA+(hzJNJmvcu+;n==TB zN%Gju7a(m8A?I6C5ckd@Jr~JW;2PT;A?1QL$Od{ikII*GscqRTfAjw1=lY4D3vpOr z8bD#^bF>o1uROvljF*6UbAZHR4mv+~rmv7D+96q7BJj$eR0TAw?7<@mhvCE~@dALI z3;g2ATk_Ygstz*!Bp3WS^_Hj~=A&+h||= z0>9})6!t9w80g6)-WGsp4}5;-I>+@t1Ml7%%m_x}Ja W_V!!HvyLzT0000 None: + self.applyButton.setEnabled(not running) + self.cancelButton.setEnabled(running) + + +class IslandsLogic(qt.QObject): + processFinished = qt.Signal() -class IslandsLogic: - def __init__(self, progressBar): + def __init__(self, parent, progressBar) -> None: + super().__init__(parent) self.cliNode = None self.progressBar = progressBar self.outputLabelMapNodeId = None @@ -254,6 +269,7 @@ def segmentationCLICallback(self, caller, event): return status = caller.GetStatusString() if "Completed" in status or status == "Cancelled": + self.processFinished.emit() logging.info(status) del self.cliNode self.cliNode = None diff --git a/src/modules/ImageLogInstanceSegmenter/Models/SidewallSample.py b/src/modules/ImageLogInstanceSegmenter/Models/SidewallSample.py index 4131b87..160627a 100644 --- a/src/modules/ImageLogInstanceSegmenter/Models/SidewallSample.py +++ b/src/modules/ImageLogInstanceSegmenter/Models/SidewallSample.py @@ -14,6 +14,7 @@ from ltrace.slicer.helpers import ( triggerNodeModified, highlight_error, + remove_highlight, labels_to_color_node, save_path, reset_style_on_valid_text, @@ -55,7 +56,8 @@ def getDepthThreshold(self): def setup(self): self.progressBar = LocalProgressBar() - self.logic = SidewallSampleLogic(self.progressBar) + self.logic = SidewallSampleLogic(self, self.progressBar) + self.logic.processFinished.connect(lambda: self.updateButtonsEnablement(False)) formLayout = qt.QFormLayout(self) formLayout.setLabelAlignment(qt.Qt.AlignRight) @@ -139,6 +141,7 @@ def setup(self): self.applyButton.clicked.connect(self.onApplyButtonClicked) self.cancelButton = qt.QPushButton("Cancel") + self.cancelButton.setObjectName("Sidewall Sample Cancel Button " + self.identifier) self.cancelButton.setFixedHeight(40) self.cancelButton.clicked.connect(self.onCancelButtonClicked) @@ -148,6 +151,7 @@ def setup(self): formLayout.addRow(buttonsHBoxLayout) formLayout.addRow(self.progressBar) + self.updateButtonsEnablement(running=False) def onAmplitudeImageNodeChanged(self, itemId): amplitudeImage = slicer.mrmlScene.GetSubjectHierarchyNode().GetItemDataNode(itemId) @@ -184,6 +188,10 @@ def onApplyButtonClicked(self): highlight_error(self.outputPrefixLineEdit) return + remove_highlight(self.amplitudeImageNodeComboBox) + remove_highlight(self.transitTimeImageNodeComboBox) + remove_highlight(self.outputPrefixLineEdit) + nominalDepthsDataFrame = self.logic.readNominalDepthsCSV(self.nominalDepthsPathLineEdit.currentPath) save_path(self.nominalDepthsPathLineEdit) self.instanceSegmenterClass.set_setting("model", self.instanceSegmenterWidget.modelComboBox.currentData) @@ -199,6 +207,7 @@ def onApplyButtonClicked(self): initialDepth=-1, finalDepth=-1, ) + self.updateButtonsEnablement(running=True) self.logic.apply(segmentParameters) except MaskRCNNInfo as e: slicer.util.infoDisplay(str(e)) @@ -207,9 +216,16 @@ def onApplyButtonClicked(self): def onCancelButtonClicked(self): self.logic.cancel() + def updateButtonsEnablement(self, running: bool) -> None: + self.applyButton.setEnabled(not running) + self.cancelButton.setEnabled(running) + + +class SidewallSampleLogic(qt.QObject): + processFinished = qt.Signal() -class SidewallSampleLogic: - def __init__(self, progressBar): + def __init__(self, parent, progressBar) -> None: + super().__init__(parent) self.cliNode = None self.progressBar = progressBar self.outputLabelMapNodeId = None @@ -290,6 +306,7 @@ def instanceSegmenterCLICallback(self, caller, event): return status = caller.GetStatusString() if "Completed" in status or status == "Cancelled": + self.processFinished.emit() logging.info(status) del self.cliNode self.cliNode = None diff --git a/src/modules/ImageLogInstanceSegmenter/Models/Snow.py b/src/modules/ImageLogInstanceSegmenter/Models/Snow.py index 710c339..576c8fd 100644 --- a/src/modules/ImageLogInstanceSegmenter/Models/Snow.py +++ b/src/modules/ImageLogInstanceSegmenter/Models/Snow.py @@ -15,6 +15,7 @@ makeNodeTemporary, triggerNodeModified, highlight_error, + remove_highlight, labels_to_color_node, reset_style_on_valid_text, themeIsDark, @@ -58,7 +59,8 @@ def getSizeMinThreshold(self): def setup(self): self.progressBar = LocalProgressBar() - self.logic = SnowLogic(self.progressBar) + self.logic = SnowLogic(self, self.progressBar) + self.logic.processFinished.connect(lambda: self.updateButtonsEnablement(False)) formLayout = qt.QFormLayout(self) formLayout.setLabelAlignment(qt.Qt.AlignRight) @@ -79,7 +81,7 @@ def setup(self): mainName="Binary segmentation image", objectNamePrefix="Snow", ) - self.segmentationNodeComboBox.onMainSelected = self.onSegmentationNodeChanged + self.segmentationNodeComboBox.onMainSelectedSignal.connect(self.onSegmentationNodeChanged) self.segmentationNodeComboBox.setToolTip("Select the binary segmentation image.") inputFormLayout.addRow(self.segmentationNodeComboBox) inputFormLayout.addRow(" ", None) @@ -155,6 +157,7 @@ def setup(self): self.applyButton.clicked.connect(self.onApplyButtonClicked) self.cancelButton = qt.QPushButton("Cancel") + self.cancelButton.objectName = "Snow Cancel Button" self.cancelButton.setFixedHeight(40) self.cancelButton.clicked.connect(self.onCancelButtonClicked) @@ -164,6 +167,7 @@ def setup(self): formLayout.addRow(buttonsHBoxLayout) formLayout.addRow(self.progressBar) + self.updateButtonsEnablement(running=False) def _onMinDistanceFilterChanged(self, value, labelWidget): node = self.segmentationNodeComboBox.mainInput.currentNode() @@ -196,6 +200,9 @@ def onApplyButtonClicked(self): highlight_error(self.outputPrefixLineEdit) return + remove_highlight(self.segmentationNodeComboBox) + remove_highlight(self.outputPrefixLineEdit) + node = self.segmentationNodeComboBox.mainInput.currentNode() is_labelmap = isinstance(node, slicer.vtkMRMLLabelMapVolumeNode) if is_labelmap: @@ -236,6 +243,7 @@ def onApplyButtonClicked(self): sizeMinThreshold=float(self.sizeMinThreshold.value), outputPrefix=self.outputPrefixLineEdit.text, ) + self.updateButtonsEnablement(running=True) self.logic.apply(segmentParameters) except SnowInfo as e: slicer.util.infoDisplay(str(e)) @@ -248,9 +256,16 @@ def onApplyButtonClicked(self): def onCancelButtonClicked(self): self.logic.cancel() + def updateButtonsEnablement(self, running: bool) -> None: + self.applyButton.setEnabled(not running) + self.cancelButton.setEnabled(running) + + +class SnowLogic(qt.QObject): + processFinished = qt.Signal() -class SnowLogic: - def __init__(self, progressBar): + def __init__(self, parent, progressBar) -> None: + super().__init__(parent) self.cliNode = None self.progressBar = progressBar self.outputLabelMapNode = None @@ -325,6 +340,7 @@ def segmentationCLICallback(self, caller, event): errors = caller.GetParameterAsString("errors") status = caller.GetStatusString() if "Completed" in status or status == "Cancelled": + self.processFinished.emit() logging.info(status) self.cliNode = None if errors: diff --git a/src/modules/ImageLogSegmenter/ImageLogSegmenter.py b/src/modules/ImageLogSegmenter/ImageLogSegmenter.py index 561bf9e..1bd76a2 100644 --- a/src/modules/ImageLogSegmenter/ImageLogSegmenter.py +++ b/src/modules/ImageLogSegmenter/ImageLogSegmenter.py @@ -39,9 +39,9 @@ def setup(self): formLayout = qt.QFormLayout(frame) formLayout.setLabelAlignment(qt.Qt.AlignRight) - customizedSegmentEditorWidget = slicer.modules.customizedsegmenteditor.createNewWidgetRepresentation() - customizedSegmentEditorWidget.self().selectParameterNodeByTag(ImageLogSegmenter.SETTING_KEY) - self.segmentEditorWidget = customizedSegmentEditorWidget.self().editor + self.customizedSegmentEditorWidget = slicer.modules.customizedsegmenteditor.createNewWidgetRepresentation() + self.customizedSegmentEditorWidget.self().selectParameterNodeByTag(ImageLogSegmenter.SETTING_KEY) + self.segmentEditorWidget = self.customizedSegmentEditorWidget.self().editor self.configureEffects() self.segmentEditorWidget.unorderedEffectsVisible = False self.segmentEditorWidget.setAutoShowSourceVolumeNode(False) @@ -53,13 +53,26 @@ def setup(self): self.layout.addStretch() + self.lastSegUpdate = None + def segmentationNodeOrSourceVolumeNodeChanged(self): - if slicer.util.selectedModule() == "ImageLogEnv": - segmentationNode = self.segmentEditorWidget.segmentationNode() - sourceVolumeNode = self.segmentEditorWidget.sourceVolumeNode() - if segmentationNode is not None: - segmentationNode.SetAttribute("ImageLogSegmentation", "True") - self.logic.imageLogDataLogic.segmentationNodeOrSourceVolumeNodeChanged(segmentationNode, sourceVolumeNode) + if not slicer.util.selectedModule() == "ImageLogEnv": + return + + segmentationNode = self.segmentEditorWidget.segmentationNode() + sourceVolumeNode = self.segmentEditorWidget.sourceVolumeNode() + + # Deduplicate calls to this method when both are updated at once + segId = segmentationNode.GetID() if segmentationNode is not None else None + sourceId = sourceVolumeNode.GetID() if sourceVolumeNode is not None else None + params = segId, sourceId + if params == self.lastSegUpdate: + return + self.lastSegUpdate = params + + if segmentationNode is not None: + segmentationNode.SetAttribute("ImageLogSegmentation", "True") + self.logic.imageLogDataLogic.segmentationNodeOrSourceVolumeNodeChanged(segmentationNode, sourceVolumeNode) def onSourceVolumeNodeChanged(self, node): color_support = node and node.GetImageData() and node.GetImageData().GetNumberOfScalarComponents() == 3 @@ -105,6 +118,10 @@ def exit(self): # Leaving the module sets the active effect to "None" self.segmentEditorWidget.setActiveEffectByName("None") + def cleanup(self): + super().cleanup() + self.customizedSegmentEditorWidget.self().cleanup() + class ImageLogSegmenterLogic(LTracePluginLogic): def __init__(self): diff --git a/src/modules/ImageTools/ImageTools.py b/src/modules/ImageTools/ImageTools.py index 29fb4e2..478d0af 100644 --- a/src/modules/ImageTools/ImageTools.py +++ b/src/modules/ImageTools/ImageTools.py @@ -236,7 +236,7 @@ def onImageComboBoxCurrentNodeChanged(self, node): self.toolLabel.setVisible(node is not None) self.toolComboBox.setVisible(node is not None) - if node is not None: + if node is not None and node.GetImageData() is not None: node.GetDisplayNode().SetAutoWindowLevel(0) # To avoid brightness adjustments cancellation self.imageArray = slicer.util.arrayFromVolume(node).copy() self.showImageButton.enabled = True diff --git a/src/modules/ImportCoreImagesCLI/ImportCoreImagesCLI.py b/src/modules/ImportCoreImagesCLI/ImportCoreImagesCLI.py index 0d59134..72a6b8e 100644 --- a/src/modules/ImportCoreImagesCLI/ImportCoreImagesCLI.py +++ b/src/modules/ImportCoreImagesCLI/ImportCoreImagesCLI.py @@ -7,11 +7,11 @@ import itertools import pickle import cv2 -from ltrace.assets import get_trained_model -from ltrace.cli_progress import ProgressBarClient, StoppedError -from ltrace.image.segmentation import TF_RGBImageArrayBinarySegmenter +from pathlib import Path from ltrace.units import global_unit_registry as ureg -from ltrace.wrappers import sanitize_file_path +from ltrace.image.segmentation import TF_RGBImageArrayBinarySegmenter +from ltrace.assets import get_asset +from ltrace.cli_progress import ProgressBarClient, StoppedError def find_plug_holes(images_folder, progress_bar): @@ -31,7 +31,7 @@ def find_plug_holes(images_folder, progress_bar): if progress_bar.should_stop: raise StoppedError() - model = TF_RGBImageArrayBinarySegmenter(get_trained_model("unet-binary-segop.h5")) + model = TF_RGBImageArrayBinarySegmenter(get_asset("unet-binary-segop.h5")) for image in all_images: results.append([image, _find_plug_holes_in_file(image, model, progress_bar)]) @@ -211,8 +211,8 @@ def _find_plug_holes(core, i): ) sys.exit(1) - images_folder = sanitize_file_path(sys.argv[1]) - output_file = sanitize_file_path(sys.argv[2]) + images_folder = Path(sys.argv[1]).absolute() + output_file = Path(sys.argv[2]).absolute() zmq_port = int(sys.argv[3]) with ProgressBarClient(zmq_port) as progress_bar: @@ -221,5 +221,5 @@ def _find_plug_holes(core, i): result = find_plug_holes(images_folder, progress_bar) - with open(output_file.as_posix(), "wb") as f: + with open(output_file, "wb") as f: f.write(pickle.dumps(result)) diff --git a/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/imagelog.py b/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/imagelog.py index 38f5165..8314efb 100644 --- a/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/imagelog.py +++ b/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/imagelog.py @@ -86,11 +86,11 @@ def __init__(self): def segment(self, p): import tensorflow as tf - if p.model == "petrobras_sidewall_sample_mask_rcnn_1": + if p.model == "side_1": self.inferenceConfig = PetrobrasSidewallSampleConfig1() - elif p.model == "petrobras_sidewall_sample_mask_rcnn_2": + elif p.model == "side_2": self.inferenceConfig = PetrobrasSidewallSampleConfig2() - elif p.model == "synthetic_sidewall_sample_mask_rcnn": + elif p.model == "synth_side": self.inferenceConfig = SyntheticSidewallSampleConfig() else: raise RuntimeError(f"Model {p.model} isn't implemented.") diff --git a/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/model.py b/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/model.py index 0ae0666..d01a3f6 100644 --- a/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/model.py +++ b/src/modules/InstanceSegmenterCLI/InstanceSegmenterCLILib/model/model.py @@ -2,14 +2,14 @@ import numpy as np import slicer import slicer.util -from ltrace.assets_utils import get_trained_model +from ltrace.assets_utils import get_asset from ltrace.lmath.filtering import DistributionFilter from scipy.ndimage import zoom class Model: def getModelPath(self, model): - return get_trained_model("ImageLogEnv/" + model + ".h5") + return get_asset("ImageLogEnv/" + model + ".h5") def segment(self, parameters): raise NotImplementedError diff --git a/src/modules/InstanceSegmenterEditor/InstanceSegmenterEditor.py b/src/modules/InstanceSegmenterEditor/InstanceSegmenterEditor.py index a9168b2..1fe7c7f 100644 --- a/src/modules/InstanceSegmenterEditor/InstanceSegmenterEditor.py +++ b/src/modules/InstanceSegmenterEditor/InstanceSegmenterEditor.py @@ -253,7 +253,7 @@ def onApplySegmentButtonClicked(self): row = model.getRowByLabel(rowData["label"]) self.tableWidget.tableView.setCurrentIndex(model.index(row, 0)) else: - if "sidewall_sample" in self.logic.getInstanceType(): + if "side" in self.logic.getInstanceType(): del rowData["n depth (m)"] del rowData["desc"] del rowData["cond"] @@ -338,7 +338,7 @@ def onInputTableNodeChanged(self, itemId): self.tableWidget.deleteLater() instanceType = self.logic.getInstanceType() - if "sidewall_sample" in instanceType: + if "side" in instanceType: self.tableWidget = SidewallSampleTableWidget(self.logic) elif ImageLogInstanceSegmenter.MODEL_IMAGE_LOG_STOPS in instanceType: self.tableWidget = StopsTableWidget(self.logic) @@ -477,7 +477,7 @@ def applySegment(self): instanceType = self.getInstanceType() - if "sidewall_sample" in instanceType: + if "side" in instanceType: properties = sidewall_sample_instance_properties(mask, self.labelMapNode.GetSpacing()) rowData = properties rowData["label"] = self.editedLabelValue diff --git a/src/modules/JobMonitor/JobMonitor.py b/src/modules/JobMonitor/JobMonitor.py index 0d1cb3d..d4c35ff 100644 --- a/src/modules/JobMonitor/JobMonitor.py +++ b/src/modules/JobMonitor/JobMonitor.py @@ -18,7 +18,7 @@ import slicer import vtk - +from ltrace.slicer.widget.elided_label import ElidedLabel from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic from ltrace.remote.targets import TargetManager, Host @@ -63,6 +63,19 @@ def prettydt(dtt: datetime): return dtt.strftime(dt_fmt) +class ThreeWayQuestion(qt.QMessageBox): + def __init__(self, jobname, parent=None): + super().__init__(parent) + + self.setWindowTitle(f"Job {jobname} has finished") + self.setText( + "It has been more than 15 days since this task finished. Please, delete the data to free up space in the cluster." + ) + self.addButton(qt.QPushButton("Download"), qt.QMessageBox.YesRole) + self.addButton(qt.QPushButton("Delete"), qt.QMessageBox.NoRole) + self.addButton(qt.QPushButton("Close"), qt.QMessageBox.RejectRole) + + class JobListWidget(qt.QListWidget): def __init__(self, parent=None): qt.QListWidget.__init__(self, parent) @@ -87,20 +100,34 @@ class JobListItemWidget(qt.QWidget): def __init__(self, job, parent=None): qt.QWidget.__init__(self, parent) - self.setMinimumWidth(300) + self.setMinimumWidth(312) self.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred) - self.jobNameLabel = qt.QLabel(f"{job.name} (Host: {job.host.name})") + self.jobNameLabel = ElidedLabel(f"{job.name} (Host: {job.host.name})") self.jobNameLabel.setStyleSheet("QLabel {font-size: 14px; font-weight: bold;}") + self.jobNameLabel.setToolTip(f"{job.name} (Host: {job.host.name})") + self.jobNameLabel.setMaximumWidth(274) progressWidget = self.createProgressInfo() infoWidget = self.createInfoWidget(job.status) layout = qt.QHBoxLayout(self) - layout.setContentsMargins(16, 6, 6, 6) + layout.setContentsMargins(6, 6, 6, 6) + + # self.iconBtn = self.actionButton("erro", onClick=self.showMessageAboutJobAging) + self.iconBtn = qt.QPushButton("") + self.iconBtn.clicked.connect(self.showMessageAboutJobAging) + self.iconBtn.setIcon(qt.QIcon(qt.QPixmap(str(JobMonitor.RES_DIR / "Icons" / "erro.png")))) + self.iconBtn.setIconSize(qt.QSize(24, 24)) + self.iconBtn.setStyleSheet("QPushButton {padding: 2px}") + self.iconBtn.visible = False + + iconBlock = qt.QVBoxLayout() + iconBlock.setContentsMargins(6, 6, 6, 6) + iconBlock.setSpacing(6) + iconBlock.addWidget(self.iconBtn) frontBlock = qt.QVBoxLayout() frontBlock.setContentsMargins(0, 0, 0, 0) - frontBlock.addWidget(self.jobNameLabel) frontBlock.addWidget(progressWidget) frontBlock.addWidget(infoWidget) @@ -111,12 +138,10 @@ def __init__(self, job, parent=None): menuBlock.setContentsMargins(6, 6, 6, 6) menuBlock.setSpacing(6) menuBlock.addWidget(self.menuBtn) - # errorButton = self.menuIcon("erro") - # menuBlock.addWidget(errorButton, hide=False) - # errorButton.clicked.connect(self.errorClick) menuBlock.addStretch(1) + layout.addLayout(iconBlock) layout.addLayout(frontBlock) layout.addLayout(menuBlock) self.update(job) @@ -180,6 +205,15 @@ def setContextMenu(self, location: qt.QPoint): cancelAction.triggered.connect(self.onDeleteResults) menu.exec_(location) + def showMessageAboutJobAging(self): + dialog = ThreeWayQuestion(self.jobNameLabel.text) + clicked = dialog.exec_() + + if clicked == qt.QMessageBox.AcceptRole: + self.loadResults.emit(True) + elif clicked == qt.QMessageBox.RejectRole: + self.onDeleteResults(True) + def showContextMenuOnClick(self): self.setContextMenu(location=self.menuBtn.mapToGlobal(self.menuBtn.rect.topRight())) @@ -202,12 +236,15 @@ def update(self, job: JobExecutor): self.allowLoadData = False self.allowRestart = False + if JobMonitorLogic.mustIndicateAging(job): + self.iconBtn.visible = True + def onDeleteResults(self, clicked): """this function open a dialog to confirm and if yes, emit the signal to delete the results""" msg = qt.QMessageBox() msg.setIcon(qt.QMessageBox.Warning) msg.setText( - "Are you sure you want to cancel this job? This action will delete any result associated with this job." + "Are you sure you want to cancel/delete this job? This action will delete any result associated with this job on the cluster filesystem." ) msg.setWindowTitle("Warning") msg.setStandardButtons(qt.QMessageBox.Yes | qt.QMessageBox.No) @@ -382,10 +419,8 @@ def enter(self) -> None: self.update() def update(self): - print(JobManager.jobs) for uid, job in JobManager.jobs.items(): if uid not in self.listedJobs: - print("addded") self.addJob(job) else: self.updateJob(job) @@ -521,7 +556,6 @@ def updater(logic): def eventHandler(self, job, event): if event == "JOB_DELETED": - print("deletd", job.uid) self.widget.clearJob(job) else: self.widget.updateJob(job) @@ -534,3 +568,7 @@ def loadResults(self, job): JobManager.send(job.uid, "COLLECT") elif job.status == "IDLE": slicer.modules.RemoteServiceInstance.cli.resume(job) + + @staticmethod + def mustIndicateAging(job: JobExecutor): + return JobExecutor.elapsed_time(job) > datetime.timedelta(days=15) diff --git a/src/modules/LabelMapEditor/LabelMapEditor.py b/src/modules/LabelMapEditor/LabelMapEditor.py index 3c756f7..99e97e4 100644 --- a/src/modules/LabelMapEditor/LabelMapEditor.py +++ b/src/modules/LabelMapEditor/LabelMapEditor.py @@ -82,8 +82,8 @@ def readme_path(cls): # LabelMapEditorWidget # class LabelMapEditorWidget(LTracePluginWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, parent) -> None: + super().__init__(parent) self.cliNode = None self.markup = None self.edition_labelmap = None @@ -654,7 +654,7 @@ def inplace_watershed(self, volume_node, label): array = slicer.util.arrayFromVolume(volume_node) bbox = ndimage.find_objects(array)[label - 1] label_array = array[bbox] == label - dt = np.sqrt(pyedt.edt(label_array[0, :, :], force_method="cpu", closed_border=True)) + dt = pyedt.edt(label_array[0, :, :], force_method="cpu", closed_border=True) peaks = find_peaks(dt=dt) peaks = reduce_peaks(peaks) peaks = trim_saddle_points(peaks=peaks, dt=dt) @@ -963,7 +963,7 @@ def split_at_point(self, array, point): if slicer_is_in_developer_mode() and self.__in_debug_mode: cv2.imwrite(os.path.join(root, "dt_2.png"), dt.transpose()) - dt = np.sqrt(pyedt.edt(dt, force_method="cpu", closed_border=True)) + dt = pyedt.edt(dt, force_method="cpu", closed_border=True) if slicer_is_in_developer_mode() and self.__in_debug_mode: cv2.imwrite(os.path.join(root, "dt_3.png"), dt.transpose()) dt[0, point[1], point[0]] = 1 diff --git a/src/modules/MaskVolumeEffect/MaskVolumeEffectLib/SegmentEditorEffect.py b/src/modules/MaskVolumeEffect/MaskVolumeEffectLib/SegmentEditorEffect.py index c0e474c..834c2e7 100644 --- a/src/modules/MaskVolumeEffect/MaskVolumeEffectLib/SegmentEditorEffect.py +++ b/src/modules/MaskVolumeEffect/MaskVolumeEffectLib/SegmentEditorEffect.py @@ -215,6 +215,10 @@ def isVolumeVisible(self, volumeNode): return False def updateGUIFromMRML(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + self.updatingGUIFromMRML = True self.binaryMaskFillOutsideEdit.setValue( @@ -282,6 +286,10 @@ def updateGUIFromMRML(self): self.updatingGUIFromMRML = False def updateMRMLFromGUI(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + if self.updatingGUIFromMRML: return self.scriptedEffect.setParameter("FillValue", self.fillValueEdit.value) @@ -321,6 +329,10 @@ def getInputVolume(self): return inputVolume def onInputVisibilityButtonClicked(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume") sourceVolume = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode() if inputVolume is None: @@ -330,18 +342,28 @@ def onInputVisibilityButtonClicked(self): self.updateGUIFromMRML() def onOutputVisibilityButtonClicked(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume") if outputVolume: slicer.util.setSliceViewerLayers(background=outputVolume) self.updateGUIFromMRML() def onInputVolumeChanged(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return self.scriptedEffect.parameterSetNode().SetNodeReferenceID( "Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID ) self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually def onOutputVolumeChanged(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return self.scriptedEffect.parameterSetNode().SetNodeReferenceID( "Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID ) @@ -351,6 +373,10 @@ def fillValueChanged(self): self.updateMRMLFromGUI() def onApply(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + inputVolume = self.getInputVolume() outputVolume = self.outputVolumeSelector.currentNode() operationMode = self.scriptedEffect.parameter("Operation") diff --git a/src/modules/MicroCTCupsAnalysis/MicroCTCupsAnalysis.py b/src/modules/MicroCTCupsAnalysis/MicroCTCupsAnalysis.py index eb5824c..1636246 100644 --- a/src/modules/MicroCTCupsAnalysis/MicroCTCupsAnalysis.py +++ b/src/modules/MicroCTCupsAnalysis/MicroCTCupsAnalysis.py @@ -13,6 +13,7 @@ createTemporaryVolumeNode, setSourceVolume, create_color_table, + setVolumeNullValue, ) from ltrace.utils.ProgressBarProc import ProgressBarProc from ltrace.algorithms.segment_cups import segment_cups @@ -248,7 +249,8 @@ def onCupsButtonClicked(self): with ProgressBarProc() as pb: cups_callback = lambda progress, message: pb.nextStep(progress * 0.2, message) - rock, refs, cylinder = full_detect(array, callback=cups_callback) + rock, null_value, refs, cylinder = full_detect(array, callback=cups_callback) + x, y, r, z_min, z_max = cylinder offset = get_origin_offset(cylinder) if refs is not None: @@ -289,6 +291,7 @@ def onCupsButtonClicked(self): ) rockNode.CreateDefaultDisplayNodes() slicer.util.updateVolumeFromArray(rockNode, rock) + setVolumeNullValue(rockNode, null_value) rockNode.CopyOrientation(volume) origin = rockNode.GetOrigin() diff --git a/src/modules/MicroCTLoader/Libs/MicroCTLoaderBaseWidget.py b/src/modules/MicroCTLoader/Libs/MicroCTLoaderBaseWidget.py index 62155e4..40e1f0c 100644 --- a/src/modules/MicroCTLoader/Libs/MicroCTLoaderBaseWidget.py +++ b/src/modules/MicroCTLoader/Libs/MicroCTLoaderBaseWidget.py @@ -36,6 +36,7 @@ class LoadParameters: imageSpacing3: float = 1.0 * ureg.micrometer centerVolume: bool = True invertDirections: list[bool] = field(default_factory=lambda: [True, True, False]) + loadAsLabelmap: bool = False class MicroCTLoaderBaseWidget(LTracePluginWidget): @@ -46,8 +47,8 @@ class MicroCTLoaderBaseWidget(LTracePluginWidget): IMAGE_SPACING_3 = "MicroCTLoader/imageSpacing3" CENTER_VOLUME = "MicroCTLoader/centerVolume" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, parent) -> None: + super().__init__(parent) self.moduleName = "MicroCTLoader" def exit(self): @@ -98,6 +99,10 @@ def setupNormalWidget(self): self.centerVolumeCheckbox = qt.QCheckBox("Center volume") self.centerVolumeCheckbox.setChecked(self.getCenterVolume() == "True") + self.loadAsLabelmapCheckBox = qt.QCheckBox("Load as Labelmap") + self.loadAsLabelmapCheckBox.setToolTip( + "If selected, a labelmap volume will be created instead of scalar volume." + ) self.widthDirectionCheckbox = qt.QCheckBox("Width (x)") self.widthDirectionCheckbox.setChecked(True) @@ -128,6 +133,7 @@ def setupNormalWidget(self): self.invertLabel.setVisible(False) outputLayout.addRow("", self.centerVolumeCheckbox) + outputLayout.addRow("", self.loadAsLabelmapCheckBox) outputLayout.addRow(self.invertLabel, self.optionsWidgets) outputLayout.addRow(" ", None) @@ -319,6 +325,7 @@ def onLoadButtonClicked(self): self.lengthDirectionCheckbox.isChecked(), self.heightDirectionCheckbox.isChecked(), ], + self.loadAsLabelmapCheckBox.isChecked(), ) callback.on_update("Loading...", 10) node, *_ = self.logic.load(path, loadParameters) @@ -330,6 +337,9 @@ def onLoadButtonClicked(self): def _setInvertWidgetVisibility(self, isVisible): self.centerVolumeCheckbox.setVisible(isVisible) + self.loadAsLabelmapCheckBox.setVisible(isVisible) + if not isVisible: + self.loadAsLabelmapCheckBox.setChecked(qt.Qt.Unchecked) self.optionsWidgets.setVisible(isVisible) self.invertLabel.setVisible(isVisible) diff --git a/src/modules/MicroCTLoader/Libs/MicroCTLoaderLogic.py b/src/modules/MicroCTLoader/Libs/MicroCTLoaderLogic.py index 4d24208..aedc747 100644 --- a/src/modules/MicroCTLoader/Libs/MicroCTLoaderLogic.py +++ b/src/modules/MicroCTLoader/Libs/MicroCTLoaderLogic.py @@ -59,7 +59,8 @@ def loadNetCDF(self, path, p): return nodes def loadImage(self, file, p, baseName, singleFile=False): - node = slicer.util.loadVolume(str(file), properties={"singleFile": singleFile}) + loadAsLabelmap = p.loadAsLabelmap and singleFile + node = slicer.util.loadVolume(str(file), properties={"singleFile": singleFile, "labelmap": loadAsLabelmap}) spacing = [ p.imageSpacing1.m_as(SLICER_LENGTH_UNIT), diff --git a/src/modules/MicroCTLoader/Libs/RawLoader.py b/src/modules/MicroCTLoader/Libs/RawLoader.py index 6c247fd..408a6f0 100644 --- a/src/modules/MicroCTLoader/Libs/RawLoader.py +++ b/src/modules/MicroCTLoader/Libs/RawLoader.py @@ -239,7 +239,7 @@ def showOutputVolume(self): # Fitting to the output segmentation and labelmap fit = self.fitToViewsCheckBox.checked referenceVolumeNodeId = selectedVolumeNode.GetAttribute("ReferenceVolumeNode") - node = slicer.util.getNode(referenceVolumeNodeId) + node = helpers.tryGetNode(referenceVolumeNodeId) if node is not None: slicer.util.setSliceViewerLayers(label=node, fit=fit) @@ -463,7 +463,9 @@ def __centerVolume(node): transformAdded = node.AddCenteringTransform() if transformAdded: node.HardenTransform() - slicer.mrmlScene.RemoveNode(slicer.util.getNode(node.GetName() + " centering transform")) + centeringTransform = slicer.mrmlScene.GetFirstNodeByName(node.GetName() + " centering transform") + if centeringTransform is not None: + slicer.mrmlScene.RemoveNode(centeringTransform) def __updateLabelMapVolumeNodeImage( self, @@ -570,7 +572,7 @@ def __updateSegmentationNodeImage( referenceVolumeNodeId = outputVolumeNode.GetAttribute("ReferenceVolumeNode") if referenceVolumeNodeId is not None: outputVolumeNode.RemoveAttribute("ReferenceVolumeNode") - node = slicer.util.getNode(referenceVolumeNodeId) + node = helpers.tryGetNode(referenceVolumeNodeId) slicer.mrmlScene.RemoveNode(node) del node diff --git a/src/modules/MicroCTTransforms/MicroCTTransforms.py b/src/modules/MicroCTTransforms/MicroCTTransforms.py index e8b4753..d44cf84 100644 --- a/src/modules/MicroCTTransforms/MicroCTTransforms.py +++ b/src/modules/MicroCTTransforms/MicroCTTransforms.py @@ -8,6 +8,7 @@ import vtk from Customizer import Customizer from ltrace.slicer_utils import * +from ltrace.slicer.helpers import BlockSignals def normalize_angle(angle): @@ -91,6 +92,7 @@ def setup(self): self.rotationSliders.layout().replaceWidget(rotationBox, rotationDials) rotationBox.hide() + self.dials = [] self.lastRotationValues = [0, 0, 0] self.sliderCumulativeDelta = [0, 0, 0] for i, sliderName in enumerate(["LRSlider", "PASlider", "ISSlider"]): @@ -106,6 +108,7 @@ def setup(self): dial.notchesVisible = True dial.setToolTip(f"Click and drag to rotate dial") gridLayout.addWidget(dial, 1, i) + self.dials.append(dial) incrementLayout = qt.QHBoxLayout() incrementSpinBox = ctk.ctkDoubleSpinBox() @@ -155,8 +158,10 @@ def increment(*args, dial=dial, incBox=incrementSpinBox, mul=mul): vBoxLayout = qt.QVBoxLayout() self.transformToolButton = transformWidget.findChild(qt.QObject, "TransformToolButton") + self.transformToolButton.clicked.connect(self.onTransformedVolumeChanged) vBoxLayout.addWidget(self.transformToolButton) self.untransformToolButton = transformWidget.findChild(qt.QObject, "UntransformToolButton") + self.untransformToolButton.clicked.connect(self.onTransformedVolumeChanged) vBoxLayout.addWidget(self.untransformToolButton) hBoxLayout.addLayout(vBoxLayout) @@ -167,11 +172,11 @@ def increment(*args, dial=dial, incBox=incrementSpinBox, mul=mul): vBoxLayout.addWidget(self.transformedTreeView) hBoxLayout.addLayout(vBoxLayout) - displayEditCollapsibleWidget = transformWidget.findChild( + self.displayEditCollapsibleWidget = transformWidget.findChild( ctk.ctkCollapsibleButton, "DisplayEditCollapsibleWidget" ) - displayEditCollapsibleWidget.setText("Parameters") - formLayout.addRow(displayEditCollapsibleWidget) + self.displayEditCollapsibleWidget.setText("Parameters") + formLayout.addRow(self.displayEditCollapsibleWidget) self.reflectLRButton = qt.QPushButton("Reflect X") self.reflectLRButton.clicked.connect(self.onReflectLRButton) @@ -185,10 +190,29 @@ def increment(*args, dial=dial, incBox=incrementSpinBox, mul=mul): reflectButtonsHBoxLayout.addWidget(self.reflectLRButton) reflectButtonsHBoxLayout.addWidget(self.reflectPAButton) reflectButtonsHBoxLayout.addWidget(self.reflectISButton) - displayEditCollapsibleWidget.layout().addWidget(frame) + self.displayEditCollapsibleWidget.layout().addWidget(frame) + + transposeGroup = qt.QGroupBox() + transposeGroup.setTitle("Transpose axes") + transposeLayout = qt.QHBoxLayout(transposeGroup) + transposeLayout.addStretch(0.1) + transposeLayout.addWidget(qt.QLabel("X Y Z \u2192")) + + self.transposeComboBox = qt.QComboBox() + self.transposeComboBox.addItems(["X Z Y", "Y X Z", "Y Z X", "Z X Y", "Z Y X"]) + transposeLayout.addWidget(self.transposeComboBox) + + transposeButton = qt.QPushButton("Transpose") + transposeButton.clicked.connect(lambda: self.onTranspose(self.transposeComboBox.currentText)) + transposeLayout.addWidget(transposeButton) + transposeLayout.addStretch(1) + self.displayEditCollapsibleWidget.layout().addWidget(transposeGroup) formLayout.addRow(" ", None) + self.buttonsWidget = qt.QFrame() + buttonsLayout = qt.QFormLayout(self.buttonsWidget) + self.undoButton = qt.QPushButton("Undo") self.undoButton.setIcon(qt.QIcon(str(Customizer.UNDO_ICON_PATH))) self.undoButton.setToolTip( @@ -206,7 +230,7 @@ def increment(*args, dial=dial, incBox=incrementSpinBox, mul=mul): buttonsHBoxLayout = qt.QHBoxLayout() buttonsHBoxLayout.addWidget(self.undoButton) buttonsHBoxLayout.addWidget(self.redoButton) - formLayout.addRow(buttonsHBoxLayout) + buttonsLayout.addRow(buttonsHBoxLayout) self.applyButton = qt.QPushButton("Apply") self.applyButton.setIcon(qt.QIcon(str(Customizer.APPLY_ICON_PATH))) @@ -223,9 +247,20 @@ def increment(*args, dial=dial, incBox=incrementSpinBox, mul=mul): applyResetButtonsHBoxLayout = qt.QHBoxLayout() applyResetButtonsHBoxLayout.addWidget(self.applyButton) applyResetButtonsHBoxLayout.addWidget(self.resetButton) - formLayout.addRow(applyResetButtonsHBoxLayout) + buttonsLayout.addRow(applyResetButtonsHBoxLayout) + + formLayout.addRow(self.buttonsWidget) self.layout.addStretch(1) + self.onTransformedVolumeChanged() + + def onTransformedVolumeChanged(self): + slicer.app.processEvents(1000) + self.transformedTreeView.selectAll() + visible = len(self.transformedTreeView.selectedIndexes()) > 0 + self.transformedTreeView.clearSelection() + self.buttonsWidget.visible = visible + self.displayEditCollapsibleWidget.visible = visible def updateSlider(self, sliderIndex, slider, value): # Value from dial is integer, but we want to have 1 decimal place precision @@ -273,19 +308,38 @@ def onApplyButtonClicked(self): self.configureButtonsState() def onResetButtonClicked(self): + for dial in self.dials: + with BlockSignals(dial): + dial.setValue(0) + self.setRotationSlidersValues([0, 0, 0]) + self.sliderCumulativeDelta = [0, 0, 0] + self.transformedTreeView.selectAll() self.untransformToolButton.click() self.renewHiddenTransformNode() self.configureButtonsState() + def reflect(self, plane): + transformNode = self.transformNodeSelector.currentNode() + slicer.mrmlScene.SaveStateForUndo(transformNode) + self.logic.reflect(transformNode, plane) + self.configureButtonsState() + def onReflectLRButton(self): - self.logic.reflect(self.transformNodeSelector.currentNode(), "LR") + self.reflect("LR") def onReflectPAButton(self): - self.logic.reflect(self.transformNodeSelector.currentNode(), "PA") + self.reflect("PA") def onReflectISButton(self): - self.logic.reflect(self.transformNodeSelector.currentNode(), "IS") + self.reflect("IS") + + def onTranspose(self, order): + transformNode = self.transformNodeSelector.currentNode() + slicer.mrmlScene.SaveStateForUndo(transformNode) + order = order.replace(" ", "") + self.logic.transpose(transformNode, order) + self.configureButtonsState() def renewHiddenTransformNode(self): self.setRotationSlidersValues([0, 0, 0]) @@ -302,11 +356,8 @@ def renewHiddenTransformNode(self): ) def onTransformNodeModified(self, *args): - self.applyButton.enabled = False - self.resetButton.enabled = False - self.undoButton.enabled = False - self.redoButton.enabled = False - + self.applyButton.enabled = True + self.resetButton.enabled = True self.transformInProgress = True def enter(self): @@ -320,6 +371,7 @@ def exit(self): slicer.mrmlScene.SetUndoOff() self.transformNodeSelector.currentNode().RemoveObserver(slicer.vtkMRMLTransformNode.TransformModifiedEvent) slicer.mrmlScene.RemoveNode(self.transformNodeSelector.currentNode()) + self.onTransformedVolumeChanged() def configureButtonsState(self): numberOfUndoLevels = slicer.mrmlScene.GetNumberOfUndoLevels() @@ -358,14 +410,28 @@ def reflect(self, transformNode, plane): matrixArray = slicer.util.arrayFromVTKMatrix(vtkMatrix) if plane == "LR": if np.isclose(abs(matrixArray[0, 0]), 1): - matrixArray[0, 0] *= -1 + matrixArray[:, 0] *= -1 elif plane == "PA": if np.isclose(abs(matrixArray[1, 1]), 1): - matrixArray[1, 1] *= -1 + matrixArray[:, 1] *= -1 elif plane == "IS": if np.isclose(abs(matrixArray[2, 2]), 1): - matrixArray[2, 2] *= -1 + matrixArray[:, 2] *= -1 + + vtkTransformationMatrix = vtk.vtkMatrix4x4() + vtkTransformationMatrix.DeepCopy(list(np.array(matrixArray).flat)) + transformNode.SetMatrixTransformToParent(vtkTransformationMatrix) + + def transpose(self, transformNode, order): + vtkMatrix = transformNode.GetMatrixTransformToParent() + matrixArray = slicer.util.arrayFromVTKMatrix(vtkMatrix) + columns = matrixArray.T + reordered = columns.copy() + map = {"X": 0, "Y": 1, "Z": 2} + for i, axis in enumerate(order): + reordered[i] = columns[map[axis]] + matrixArray = reordered.T vtkTransformationMatrix = vtk.vtkMatrix4x4() vtkTransformationMatrix.DeepCopy(list(np.array(matrixArray).flat)) transformNode.SetMatrixTransformToParent(vtkTransformationMatrix) diff --git a/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry.py b/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry.py index e77782c..e38d43d 100644 --- a/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry.py +++ b/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry.py @@ -8,7 +8,6 @@ # Funções do tipo run_psd estão importando apenas os .raw para análise, quando deveriam importar também .nc. # Fazer transformação entre radii X Sw e Pc X Sw de maneira simples. -import logging import sys, time, os, datetime import numpy as np import xarray as xr @@ -59,71 +58,64 @@ def psd(input_data, sat_resolution=0.03, rad_resolution=0.5, output_file_path=". output_path = os.path.dirname(os.path.abspath(output_file_path)) if not isdir(output_path): os.makedirs(output_path) - try: - output_psd_file = open(output_file_path, "a") + output_psd_file = open(output_file_path, "a") - start = time.time() - porosity = bin_data.mean() - bin_edt = ndimage.distance_transform_edt(bin_data) - max_radius = bin_edt.max() + start = time.time() + porosity = bin_data.mean() + bin_edt = ndimage.distance_transform_edt(bin_data) + max_radius = bin_edt.max() + + nw_saturation = np.zeros(3, dtype=np.float32) + list_radii = np.array([1, max_radius / 4, max_radius], dtype=np.float32) + for i in range(len(list_radii)): + nw_image = np.multiply( + bin_data, + (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]).astype(np.uint8), + dtype=np.uint8, + ) + if verbose: + output_image = np.maximum(output_image, nw_image * list_radii[i]) + nw_saturation[i] = nw_image.mean() / porosity + print("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i])) + output_psd_file.write("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i]) + "\n") + output_psd_file.flush() + + more_radii = True + cur_resolution = np.max([nw_saturation[0] - nw_saturation[1], nw_saturation[1] - nw_saturation[2]]) + while (cur_resolution > sat_resolution) and (more_radii): + more_radii = False + new_list_radii = list_radii + new_nw_saturation = nw_saturation + for i in range(len(list_radii) - 1): + if np.abs(nw_saturation[i + 1] - nw_saturation[i]) > sat_resolution: + if np.abs(list_radii[i + 1] - list_radii[i]) > 2 * rad_resolution: + new_list_radii = np.append(new_list_radii, (list_radii[i + 1] + list_radii[i]) / 2) + new_nw_saturation = np.append(new_nw_saturation, -1) + more_radii = True + nw_saturation = new_nw_saturation + list_radii = new_list_radii - nw_saturation = np.zeros(3, dtype=np.float32) - list_radii = np.array([1, max_radius / 4, max_radius], dtype=np.float32) for i in range(len(list_radii)): - nw_image = np.multiply( - bin_data, - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]).astype(np.uint8), - dtype=np.uint8, - ) - if verbose: - output_image = np.maximum(output_image, nw_image * list_radii[i]) - nw_saturation[i] = nw_image.mean() / porosity - print("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i])) - output_psd_file.write("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i]) + "\n") - output_psd_file.flush() - - more_radii = True - cur_resolution = np.max([nw_saturation[0] - nw_saturation[1], nw_saturation[1] - nw_saturation[2]]) - while (cur_resolution > sat_resolution) and (more_radii): - more_radii = False - new_list_radii = list_radii - new_nw_saturation = nw_saturation - for i in range(len(list_radii) - 1): - if np.abs(nw_saturation[i + 1] - nw_saturation[i]) > sat_resolution: - if np.abs(list_radii[i + 1] - list_radii[i]) > 2 * rad_resolution: - new_list_radii = np.append(new_list_radii, (list_radii[i + 1] + list_radii[i]) / 2) - new_nw_saturation = np.append(new_nw_saturation, -1) - more_radii = True - nw_saturation = new_nw_saturation - list_radii = new_list_radii - - for i in range(len(list_radii)): - if nw_saturation[i] < 0: - nw_image = np.multiply( - bin_data, - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]).astype( - np.uint8 - ), - dtype=np.uint8, - ) - if verbose: - output_image = np.maximum(output_image, nw_image * list_radii[i]) - nw_saturation[i] = nw_image.mean() / porosity - print("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i])) - output_psd_file.write("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i]) + "\n") - output_psd_file.flush() - list_radii = np.sort(list_radii) - nw_saturation = np.sort(nw_saturation)[::-1] - - cur_resolution = 0 - for i in range(len(list_radii) - 1): - cur_resolution = np.max([cur_resolution, np.abs(nw_saturation[i + 1] - nw_saturation[i])]) - except Exception as error: - logging.debug(f"Error: {error}") - finally: - if output_psd_file: - output_psd_file.close() + if nw_saturation[i] < 0: + nw_image = np.multiply( + bin_data, + (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]).astype(np.uint8), + dtype=np.uint8, + ) + if verbose: + output_image = np.maximum(output_image, nw_image * list_radii[i]) + nw_saturation[i] = nw_image.mean() / porosity + print("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i])) + output_psd_file.write("Radius: " + str(list_radii[i]) + ", Snw: " + str(nw_saturation[i]) + "\n") + output_psd_file.flush() + list_radii = np.sort(list_radii) + nw_saturation = np.sort(nw_saturation)[::-1] + + cur_resolution = 0 + for i in range(len(list_radii) - 1): + cur_resolution = np.max([cur_resolution, np.abs(nw_saturation[i + 1] - nw_saturation[i])]) + output_psd_file.close() print("Time spent in psd: " + str(time.time() - start)) if verbose: if type(input_data) == np.ndarray: @@ -386,66 +378,58 @@ def drainage_incompressible( output_path = os.path.dirname(os.path.abspath(output_file_path)) if not isdir(output_path): os.makedirs(output_path) + output_drai_file = open(output_file_path, "a") + + start = time.time() + porosity = bin_data.mean() + bin_edt = ndimage.distance_transform_edt(bin_data) + max_radius = bin_edt.max() - try: - output_drai_file = open(output_file_path, "a") - start = time.time() - porosity = bin_data.mean() - bin_edt = ndimage.distance_transform_edt(bin_data) - max_radius = bin_edt.max() + list_radii = np.array( + [max_radius, (3.0 * max_radius / 4), (max_radius / 2), (max_radius / 4), 1.0], dtype=np.float32 + ) - list_radii = np.array( - [max_radius, (3.0 * max_radius / 4), (max_radius / 2), (max_radius / 4), 1.0], dtype=np.float32 - ) + not_found = True + while not_found: + nw_saturation = [] + unconnected_phase = np.zeros(bin_data.shape) - not_found = True - while not_found: - nw_saturation = [] - unconnected_phase = np.zeros(bin_data.shape) + if verbose: + output_image = 0.5 * bin_data.copy() + for i in range(len(list_radii)): + nw_image = connected_image( + np.multiply( + bin_data - unconnected_phase, + ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i], + ), + direction=direction, + ) + unconnected_phase = bin_data - nw_image - connected_image(bin_data - nw_image, direction="z+") if verbose: - output_image = 0.5 * bin_data.copy() + output_image = np.maximum(output_image, nw_image * list_radii[i]) + nw_saturation.append((nw_image.mean() / porosity)) + print("Radius: " + str(list_radii[i]) + ", Saturation: " + str(nw_saturation[-1])) + output_drai_file.write("Radius: " + str(list_radii[i]) + ", Saturation: " + str(nw_saturation[-1]) + "\n") + output_drai_file.flush() - for i in range(len(list_radii)): - nw_image = connected_image( - np.multiply( - bin_data - unconnected_phase, - ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i], - ), - direction=direction, - ) - unconnected_phase = bin_data - nw_image - connected_image(bin_data - nw_image, direction="z+") - if verbose: - output_image = np.maximum(output_image, nw_image * list_radii[i]) - nw_saturation.append((nw_image.mean() / porosity)) - print("Radius: " + str(list_radii[i]) + ", Saturation: " + str(nw_saturation[-1])) - output_drai_file.write( - "Radius: " + str(list_radii[i]) + ", Saturation: " + str(nw_saturation[-1]) + "\n" - ) - output_drai_file.flush() - - print("list_radii: " + str(list_radii)) - print("nw_saturation: " + str(nw_saturation)) - new_list_radii = list_radii - number_of_radii = len(new_list_radii) - for i in range(1, len(nw_saturation)): - if nw_saturation[i] - nw_saturation[i - 1] > sat_resolution: - if list_radii[i - 1] - list_radii[i] > rad_resolution: - new_list_radii = np.append( - new_list_radii, list_radii[i] + (list_radii[i - 1] - list_radii[i]) / 3 - ) - new_list_radii = np.append( - new_list_radii, list_radii[i] + 2.0 * (list_radii[i - 1] - list_radii[i]) / 3 - ) - new_list_radii = np.sort(new_list_radii)[::-1] - if number_of_radii == len(new_list_radii): - not_found = False - list_radii = new_list_radii - except Exception as error: - logging.debug(f"Error: {error}") - finally: - if output_drai_file: - output_drai_file.close() + print("list_radii: " + str(list_radii)) + print("nw_saturation: " + str(nw_saturation)) + new_list_radii = list_radii + number_of_radii = len(new_list_radii) + for i in range(1, len(nw_saturation)): + if nw_saturation[i] - nw_saturation[i - 1] > sat_resolution: + if list_radii[i - 1] - list_radii[i] > rad_resolution: + new_list_radii = np.append(new_list_radii, list_radii[i] + (list_radii[i - 1] - list_radii[i]) / 3) + new_list_radii = np.append( + new_list_radii, list_radii[i] + 2.0 * (list_radii[i - 1] - list_radii[i]) / 3 + ) + new_list_radii = np.sort(new_list_radii)[::-1] + if number_of_radii == len(new_list_radii): + not_found = False + list_radii = new_list_radii + + output_drai_file.close() print("Time spent in drainage: " + str(time.time() - start)) if verbose: @@ -512,76 +496,68 @@ def imbibition_compressible( output_path = os.path.dirname(os.path.abspath(output_file_path)) if not isdir(output_path): os.makedirs(output_path) + output_imb_file = open(output_file_path, "a") - try: - output_imb_file = open(output_file_path, "a") + start = time.time() + porosity = bin_data.mean() + bin_edt = ndimage.distance_transform_edt(bin_data) + max_radius = bin_edt.max() - start = time.time() - porosity = bin_data.mean() - bin_edt = ndimage.distance_transform_edt(bin_data) - max_radius = bin_edt.max() + w_saturation = np.zeros(3, dtype=np.float32) + list_radii = np.array([1, max_radius / 2.0, max_radius], dtype=np.float32) + for i in range(len(list_radii)): + w_image = connected_image( + np.multiply(bin_data, 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i])), + direction=direction, + ) + if verbose: + output_image = np.multiply(w_image, np.minimum(output_image, w_image * list_radii[i])) + np.multiply( + (1 - w_image), output_image + ) + w_saturation[i] = w_image.mean() / porosity + print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i])) + output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i]) + "\n") + output_imb_file.flush() + + more_radii = True + cur_resolution = np.max([w_saturation[1] - w_saturation[0], w_saturation[2] - w_saturation[1]]) + while (cur_resolution > sat_resolution) and (more_radii): + more_radii = False + new_list_radii = list_radii + new_w_saturation = w_saturation + for i in range(len(list_radii) - 1): + if np.abs(w_saturation[i + 1] - w_saturation[i]) > sat_resolution: + if np.abs(list_radii[i + 1] - list_radii[i]) > 2 * rad_resolution: + new_list_radii = np.append(new_list_radii, (list_radii[i + 1] + list_radii[i]) / 2) + new_w_saturation = np.append(new_w_saturation, -1) + more_radii = True + w_saturation = new_w_saturation + list_radii = new_list_radii - w_saturation = np.zeros(3, dtype=np.float32) - list_radii = np.array([1, max_radius / 2.0, max_radius], dtype=np.float32) for i in range(len(list_radii)): - w_image = connected_image( - np.multiply( - bin_data, 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]) - ), - direction=direction, - ) - if verbose: - output_image = np.multiply(w_image, np.minimum(output_image, w_image * list_radii[i])) + np.multiply( - (1 - w_image), output_image + if w_saturation[i] < 0: + w_image = connected_image( + np.multiply( + bin_data, 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]) + ), + direction=direction, ) - w_saturation[i] = w_image.mean() / porosity - print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i])) - output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i]) + "\n") - output_imb_file.flush() + if verbose: + output_image = np.multiply( + w_image, np.minimum(output_image, w_image * list_radii[i]) + ) + np.multiply(1 - w_image, output_image) + w_saturation[i] = w_image.mean() / porosity + print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i])) + output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i]) + "\n") + output_imb_file.flush() + list_radii = np.sort(list_radii) + w_saturation = np.sort(w_saturation) - more_radii = True - cur_resolution = np.max([w_saturation[1] - w_saturation[0], w_saturation[2] - w_saturation[1]]) - while (cur_resolution > sat_resolution) and (more_radii): - more_radii = False - new_list_radii = list_radii - new_w_saturation = w_saturation - for i in range(len(list_radii) - 1): - if np.abs(w_saturation[i + 1] - w_saturation[i]) > sat_resolution: - if np.abs(list_radii[i + 1] - list_radii[i]) > 2 * rad_resolution: - new_list_radii = np.append(new_list_radii, (list_radii[i + 1] + list_radii[i]) / 2) - new_w_saturation = np.append(new_w_saturation, -1) - more_radii = True - w_saturation = new_w_saturation - list_radii = new_list_radii - - for i in range(len(list_radii)): - if w_saturation[i] < 0: - w_image = connected_image( - np.multiply( - bin_data, - 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]), - ), - direction=direction, - ) - if verbose: - output_image = np.multiply( - w_image, np.minimum(output_image, w_image * list_radii[i]) - ) + np.multiply(1 - w_image, output_image) - w_saturation[i] = w_image.mean() / porosity - print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i])) - output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[i]) + "\n") - output_imb_file.flush() - list_radii = np.sort(list_radii) - w_saturation = np.sort(w_saturation) - - cur_resolution = 0 - for i in range(len(list_radii) - 1): - cur_resolution = np.max([cur_resolution, np.abs(w_saturation[i + 1] - w_saturation[i])]) - except Exception as error: - logging.debug(f"Error: {error}") - finally: - if output_imb_file: - output_imb_file.close() + cur_resolution = 0 + for i in range(len(list_radii) - 1): + cur_resolution = np.max([cur_resolution, np.abs(w_saturation[i + 1] - w_saturation[i])]) + + output_imb_file.close() print("Time spent in imbibition: " + str(time.time() - start)) if verbose: @@ -645,69 +621,62 @@ def imbibition_incompressible( output_path = os.path.dirname(os.path.abspath(output_file_path)) if not isdir(output_path): os.makedirs(output_path) + output_imb_file = open(output_file_path, "a") - try: - output_imb_file = open(output_file_path, "a") - start = time.time() - porosity = bin_data.mean() - bin_edt = ndimage.distance_transform_edt(bin_data) - max_radius = bin_edt.max() + start = time.time() + porosity = bin_data.mean() + bin_edt = ndimage.distance_transform_edt(bin_data) + max_radius = bin_edt.max() - list_radii = np.array( - [1, (max_radius / 4), (max_radius / 2), (3 * max_radius / 4), max_radius], dtype=np.float32 - ) + list_radii = np.array([1, (max_radius / 4), (max_radius / 2), (3 * max_radius / 4), max_radius], dtype=np.float32) - not_found = True - while not_found: - w_saturation = [] - unconnected_phase = np.zeros(bin_data.shape) + not_found = True + while not_found: + w_saturation = [] + unconnected_phase = np.zeros(bin_data.shape) - if verbose: - output_image = np.max(bin_data.shape) * bin_data.copy() + if verbose: + output_image = np.max(bin_data.shape) * bin_data.copy() - for i in range(len(list_radii)): - w_image = connected_image( - np.multiply( - bin_data - unconnected_phase, - 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]), - ), - direction=direction, + for i in range(len(list_radii)): + w_image = connected_image( + np.multiply( + bin_data - unconnected_phase, + 1 - (ndimage.distance_transform_edt(1 - (bin_edt >= list_radii[i])) < list_radii[i]), + ), + direction=direction, + ) + unconnected_phase = bin_data - w_image - connected_image(bin_data - w_image, direction="z+") + if verbose: + output_image = np.multiply(w_image, np.minimum(output_image, w_image * list_radii[i])) + np.multiply( + (1 - w_image), output_image ) - unconnected_phase = bin_data - w_image - connected_image(bin_data - w_image, direction="z+") - if verbose: - output_image = np.multiply( - w_image, np.minimum(output_image, w_image * list_radii[i]) - ) + np.multiply((1 - w_image), output_image) - w_saturation.append((w_image.mean() / porosity)) - print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[-1])) - output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[-1]) + "\n") - output_imb_file.flush() + w_saturation.append((w_image.mean() / porosity)) + print("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[-1])) + output_imb_file.write("Radius: " + str(list_radii[i]) + ", Sw: " + str(w_saturation[-1]) + "\n") + output_imb_file.flush() - print("list_radii: " + str(list_radii)) - print("Sw: " + str(w_saturation)) - new_list_radii = list_radii - number_of_radii = len(new_list_radii) - for i in range(1, len(w_saturation)): - if w_saturation[i] - w_saturation[i - 1] > sat_resolution: - if list_radii[i] - list_radii[i - 1] > rad_resolution: - new_list_radii = np.append( - new_list_radii, list_radii[i - 1] + (list_radii[i] - list_radii[i - 1]) / 3 - ) - new_list_radii = np.append( - new_list_radii, list_radii[i - 1] + 2.0 * (list_radii[i] - list_radii[i - 1]) / 3 - ) - new_list_radii.sort() - if number_of_radii == len(new_list_radii): - not_found = False - list_radii = new_list_radii + print("list_radii: " + str(list_radii)) + print("Sw: " + str(w_saturation)) + new_list_radii = list_radii + number_of_radii = len(new_list_radii) + for i in range(1, len(w_saturation)): + if w_saturation[i] - w_saturation[i - 1] > sat_resolution: + if list_radii[i] - list_radii[i - 1] > rad_resolution: + new_list_radii = np.append( + new_list_radii, list_radii[i - 1] + (list_radii[i] - list_radii[i - 1]) / 3 + ) + new_list_radii = np.append( + new_list_radii, list_radii[i - 1] + 2.0 * (list_radii[i] - list_radii[i - 1]) / 3 + ) + new_list_radii.sort() + if number_of_radii == len(new_list_radii): + not_found = False + list_radii = new_list_radii - list_radii = np.sort(list_radii) - w_saturation = np.sort(w_saturation) - except Exception as error: - logging.debug(f"Error: {error}") - finally: - if output_imb_file: - output_imb_file.close() + list_radii = np.sort(list_radii) + w_saturation = np.sort(w_saturation) + output_imb_file.close() print("Time spent in imbibition: " + str(time.time() - start)) if verbose: @@ -1164,14 +1133,9 @@ def run_generic_psd( + sim_type + '.sh | egrep -o -e "\\b[0-9]+$"` > job_id;sleep 0.2' ) - try: - job_id_file = open("job_id", "r") - job_id = int(job_id_file.readline()) - except ValueError: - job_id = -1 - finally: - if job_id_file: - job_id_file.close() + job_id_file = open("job_id", "r") + job_id = int(job_id_file.readline()) + job_id_file.close() print("job_id = " + str(job_id)) print("work_dir = " + output_path.replace(".", "") + "/" + cluster + "_" + str(job_id)) print("final_results = " + output_path.replace(".", "") + "/" + cluster + "_" + str(job_id) + ".nc") diff --git a/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry_opt.py b/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry_opt.py index c32ee26..c6576bf 100644 --- a/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry_opt.py +++ b/src/modules/MicrotomRemote/Libs/microtom/microtom/porosimetry_opt.py @@ -1156,15 +1156,9 @@ def run_generic_psd( + sim_type + '.sh | egrep -o -e "\\b[0-9]+$"` > job_id;sleep 0.2' ) - try: - job_id_file = open("job_id", "r") - job_id = int(job_id_file.readline()) - except ValueError: - job_id = -1 - finally: - if job_id_file: - job_id_file.close() - + job_id_file = open("job_id", "r") + job_id = int(job_id_file.readline()) + job_id_file.close() print("job_id = " + str(job_id)) print("work_dir = " + output_path.replace(".", "") + "/" + cluster + "_" + str(job_id)) print("final_results = " + output_path.replace(".", "") + "/" + cluster + "_" + str(job_id) + ".nc") diff --git a/src/modules/MicrotomRemote/Libs/microtom/requirements.txt b/src/modules/MicrotomRemote/Libs/microtom/requirements.txt index ee8be86..58c4ea4 100644 --- a/src/modules/MicrotomRemote/Libs/microtom/requirements.txt +++ b/src/modules/MicrotomRemote/Libs/microtom/requirements.txt @@ -4,7 +4,7 @@ numpy==1.23.1 dask-image==0.4.0 dask[complete]==2.30.0 netCDF4==1.5.4 -paramiko==2.10.3 +paramiko==3.4.0 scikit-image==0.19.2 scipy==1.8.1 xarray==2022.3.0 diff --git a/src/modules/MicrotomRemote/Libs/microtom/setup.py b/src/modules/MicrotomRemote/Libs/microtom/setup.py index cdaef04..29d9638 100644 --- a/src/modules/MicrotomRemote/Libs/microtom/setup.py +++ b/src/modules/MicrotomRemote/Libs/microtom/setup.py @@ -1,5 +1,4 @@ import io -import logging import os import re @@ -12,21 +11,12 @@ def read(filename): filename = os.path.join(os.path.dirname(__file__), filename) text_type = type("") - text = "" with io.open(filename, mode="r", encoding="utf-8") as fd: - try: - text = re.sub(text_type(r":[a-z]+:`~?(.*?)`"), text_type(r"``\1``"), fd.read()) - except Exception as error: - logging.debug(f"Error: {error}") + return re.sub(text_type(r":[a-z]+:`~?(.*?)`"), text_type(r"``\1``"), fd.read()) - return text - -try: - with open(THIS_FOLDER / "requirements.txt") as f: - requirements = f.readlines() -except Exception as error: - raise RuntimeError(f"Invalid requirements file. Error: {error}") +with open(THIS_FOLDER / "requirements.txt") as f: + requirements = f.readlines() setup( diff --git a/src/modules/MicrotomRemote/MicrotomRemote.py b/src/modules/MicrotomRemote/MicrotomRemote.py index 27f12eb..fc2d893 100644 --- a/src/modules/MicrotomRemote/MicrotomRemote.py +++ b/src/modules/MicrotomRemote/MicrotomRemote.py @@ -1,37 +1,61 @@ +import qt +import slicer +import ctk +import vtk import importlib import json import logging +import nrrd +import numpy as np import os +import pandas as pd +import random import re +import signal +import shutil +import string +import subprocess +import time import uuid -from collections import defaultdict - -import numpy as np +import psutil -from pathlib import Path +from collections import defaultdict from functools import partial - -import qt -import slicer -import ctk -import vtk - -from ltrace.constants import MAX_LOOP_ITERATIONS +from ltrace.pore_networks.functions import geo2spy +from ltrace.readers.microtom import KrelCompiler, PorosimetryCompiler, StokesKabsCompiler from ltrace.slicer import ui, helpers, widgets, data_utils as du -from ltrace.slicer.node_attributes import Tag, NodeEnvironment +from ltrace.slicer.node_attributes import Tag, NodeEnvironment, TableType from ltrace.slicer.widget.global_progress_bar import LocalProgressBar -from ltrace.slicer_utils import * +from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic +from ltrace.slicer.project_manager import ProjectManager +from ltrace.utils.ProgressBarProc import ProgressBarProc +from pathlib import Path +from PoreNetworkExtractor import PoreNetworkExtractorLogic +from PoreNetworkProduction import PoreNetworkProductionLogic +from PoreNetworkSimulationLib.OnePhaseSimulationWidget import OnePhaseSimulationWidget +from PoreNetworkSimulationLib.TwoPhaseSimulationWidget import TwoPhaseSimulationWidget, TwoPhaseParametersEditDialog +from PoreNetworkSimulationLib.PoreNetworkSimulationLogic import OnePhaseSimulationLogic, TwoPhaseSimulationLogic +from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget, MercurySimulationLogic +from MercurySimulationLib.SubscaleModelWidget import SubscaleModelWidget, SubscaleLogicDict +from ltrace.pore_networks.simulation_parameters_node import dict_to_parameter_node from RemoteTasks.OneResultSlurm import OneResultSlurmHandler -from ltrace.readers.microtom import KrelCompiler, PorosimetryCompiler, StokesKabsCompiler -from ltrace.slicer_utils import print_stack +try: + from StreamlitManager.StreamlitServerManager import StreamlitServerManager +except ImportError: + StreamlitServerManager = None +# Checks if closed source code is available try: from Test.MicrotomRemoteTest import MicrotomRemoteTest -except: - pass +except ImportError: + MicrotomRemoteTest = None # tests not deployed to final version or closed source +try: + from Test.PoreNetworkReportTest import PoreNetworkReportTest +except ImportError: + PoreNetworkReportTest = None # tests not deployed to final version or closed source WORKSPACES_REPO = "workspaces" @@ -287,7 +311,7 @@ def params(self): class DarcyKabsForm(RemoteSimulationForm): def params(self): - return super().params() | {"verbose": True} + return super().params() | {"verbose": False} class KabsRevForm(KabsForm): @@ -407,6 +431,78 @@ def params(self): } +class PNMReportForm(BaseArgsForm): + def __init__(self, parent=None, initialTag="Local") -> None: + super().__init__(parent) + + self.initialTag = initialTag + + self.parameterInputWidget = ui.hierarchyVolumeInput( + nodeTypes=["vtkMRMLTableNode"], + defaultText="Select node to load parameters from", + hasNone=True, + ) + self.parameterInputWidget.addNodeAttributeIncludeFilter(TableType.name(), TableType.PNM_INPUT_PARAMETERS.value) + self.parameterInputWidget.objectName = "SensibilityTestComboBox" + self.editParameterInput = qt.QPushButton("Edit") + self.editParameterInput.clicked.connect(self.onParameterEdit) + parameterWidget = qt.QWidget() + parameterWidget.setFixedHeight(25) + parameterInputLayout = qt.QHBoxLayout(parameterWidget) + parameterInputLayout.setMargin(0) + parameterInputLayout.addWidget(self.parameterInputWidget) + parameterInputLayout.addWidget(self.editParameterInput) + + self.subscaleModelWidget = SubscaleModelWidget(parent) + + self.wellName = qt.QTextEdit() + self.wellName.setFixedHeight(25) + self.wellName.objectName = "WellNameTextEdit" + + self.addArg( + "Sensibility Test Parameters: ", + parameterWidget, + BaseArgsForm._createSetter(self.parameterInputWidget, "currentText", {"Local": "none", "Remote": None}), + ) + + self.addArg( + "Subscale Pressure Model: ", + self.subscaleModelWidget.microscale_model_dropdown, + BaseArgsForm._createSetter(self.parameterInputWidget, "currentText", {"Local": "none", "Remote": None}), + ) + self.subscaleModelWidget.microscale_model_dropdown.objectName = "MicroscaleDropdown" + + for label, widget in self.subscaleModelWidget.parameter_widgets.items(): + self.layout().addRow(widget) + + self.addArg( + "Well Name: ", + self.wellName, + BaseArgsForm._createSetter(self.wellName, "plainText", {"Local": "", "Remote": None}), + ) + + def onParameterEdit(self): + node = self.parameterInputWidget.currentNode() + if not node: + slicer.util.infoDisplay("Please, select a node to be edited.") + return + + status, parameterNode = TwoPhaseParametersEditDialog(node).show() + if status: + self.parameterInputWidget.setCurrentNode(parameterNode) + + def setup(self): + self.showOnly(self.initialTag) + + def params(self): + return { + **super().params(), + "sensibility_parameters_node": self.parameterInputWidget.currentNode(), + "subscale_model_params": self.subscaleModelWidget.getParams(), + "well_name": self.wellName.plainText, + } + + # # MicrotomRemote # @@ -454,13 +550,26 @@ def __init__(self, parent=None): self.chosenExecutionMode = "Local" self.currentMode = "" + self.logicType = MicrotomRemoteLogic + + def _createLogic(self, logicCls: LTracePluginLogic): + if self.logic is not None and isinstance(self.logic, logicCls): + return self.logic + + if self.logic is not None: + del self.logic + + self.logic = logicCls(self.parent, self.progressBar) + self.logic.processFinished.connect(self.restartApplyButton) + + return self.logic def setup(self): LTracePluginWidget.setup(self) importlib.reload(widgets) - self.MODES = [widgets.SingleShotInputWidget] + self.MODES = [widgets.SingleShotInputWidget, widgets.BatchInputWidget] self.loadedFiles = {} @@ -472,21 +581,29 @@ def setup(self): self.layout.addWidget(self._setupInputsSection()) self.layout.addWidget(self._setupConfigSection()) self.layout.addWidget(self._setupOutput()) + self.serverManager = self._setupServerManager() + self.layout.addWidget(self.serverManager) def resetAll(): for i in range(self.configWidget.count): self.configWidget.widget(i).reset() - self.segmentInputWidget.segmentListUpdated.connect(resetAll) + self.modeWidgets[widgets.SingleShotInputWidget.MODE_NAME].segmentListUpdated.connect(resetAll) self.modeSelectors[widgets.SingleShotInputWidget.MODE_NAME].setChecked(True) self.simBtn.enabled = False + self.canBtn.enabled = False + self.canBtn.hide() + + self.serverManager.hide() # Add vertical spacer self.layout.addStretch(1) - self.logic = MicrotomRemoteLogic(self.parent, self.progressBar) + self._onSimulSelected(0) + + self._createLogic(logicCls=MicrotomRemoteLogic) def _setupMethodsSection(self): widget = ctk.ctkCollapsibleButton() @@ -495,7 +612,8 @@ def _setupMethodsSection(self): methodsLayout = qt.QFormLayout() self.simOptions = qt.QComboBox() - self.simOptions.objectName = "Simulation ComboBox" + self.simOptions.addItem("PNM Complete Workflow", "pnm") + self.simOptions.addItem("---------") self.simOptions.addItem("Pore Size Distribution", "psd") self.simOptions.addItem("Hierarquical Pore Size Distribution", "hpsd") self.simOptions.addItem("Mercury Injection Capillary Pressure", "micp") @@ -507,8 +625,14 @@ def _setupMethodsSection(self): self.simOptions.addItem("Absolute Permeability - Darcy FOAM", "darcy_kabs_foam") self.simOptions.addItem("Relative Permeability", "krel") + combo_model = self.simOptions.model() + separator_index = combo_model.index(1, 0) + separator_item = combo_model.itemFromIndex(separator_index) + separator_item.setFlags(separator_item.flags() & ~qt.Qt.ItemIsSelectable & ~qt.Qt.ItemIsEnabled) + self.simOptions.setToolTip("Select a simulation MicroTom method.") self.simOptions.currentIndexChanged.connect(self._onSimulSelected) + self.simOptions.objectName = "SimulationComboBox" methodsLayout.addRow("Select a Simulation: ", self.simOptions) layout.addLayout(methodsLayout) return widget @@ -517,18 +641,25 @@ def _setupInputsSection(self): widget = ctk.ctkCollapsibleButton() widget.text = "Inputs" layout = qt.QVBoxLayout(widget) - layout.setContentsMargins(0, 0, 0, 0) + optionsLayout = qt.QHBoxLayout() + optionsLayout.setAlignment(qt.Qt.AlignLeft) + optionsLayout.setContentsMargins(0, 0, 0, 0) self.optionsStack = qt.QStackedWidget() + self.mode_label = qt.QLabel("Select input mode:") + optionsLayout.addWidget(self.mode_label) + btn1 = qt.QRadioButton(widgets.SingleShotInputWidget.MODE_NAME) + btn1.objectName = "Single Shot Mode Radio Button" self.modeSelectors[widgets.SingleShotInputWidget.MODE_NAME] = btn1 optionsLayout.addWidget(btn1) btn1.toggled.connect(self._onModeClicked) panel1 = widgets.SingleShotInputWidget(dimensionsUnits={"px": True, "mm": True}) + panel1.objectName = "SingleShotInputWidget" - panel1.onMainSelected = self._onInputSelected - panel1.onReferenceSelected = self._onReferenceSelected + panel1.onMainSelectedSignal.connect(self._onInputSelected) + panel1.onReferenceSelectedSignal.connect(self._onReferenceSelected) self.modeWidgets[widgets.SingleShotInputWidget.MODE_NAME] = panel1 self.optionsStack.addWidget(self.modeWidgets[widgets.SingleShotInputWidget.MODE_NAME]) @@ -540,20 +671,23 @@ def _setupInputsSection(self): panel1.segmentsContainerWidget, ] - btn1.hide() + btn2 = qt.QRadioButton(widgets.BatchInputWidget.MODE_NAME) + btn2.objectName = "Batch Mode Radio Button" - # btn2 = qt.QRadioButton(widgets.BatchInputWidget.MODE_NAME) - # self.modeSelectors[widgets.BatchInputWidget.MODE_NAME] = btn2 - # optionsLayout.addWidget(btn2) - # btn2.toggled.connect(self._onModeClicked) - # panel2 = widgets.BatchInputWidget() - # panel2.onDirSelected = self._onInputSelected - # self.modeWidgets[widgets.BatchInputWidget.MODE_NAME] = panel2 - # self.optionsStack.addWidget(self.modeWidgets[widgets.BatchInputWidget.MODE_NAME]) + self.modeSelectors[widgets.BatchInputWidget.MODE_NAME] = btn2 + optionsLayout.addWidget(btn2) + btn2.toggled.connect(self._onModeClicked) + panel2 = widgets.BatchInputWidget(objectNamePrefix="Microtom Remote Batch") + panel2.onDirSelected = self._onInputSelected + self.modeWidgets[widgets.BatchInputWidget.MODE_NAME] = panel2 + self.optionsStack.addWidget(self.modeWidgets[widgets.BatchInputWidget.MODE_NAME]) + + self.mode_label.hide() + btn1.hide() + btn2.hide() layout.addLayout(optionsLayout) layout.addWidget(self.optionsStack) - self.segmentInputWidget = panel1 return widget @@ -597,7 +731,10 @@ def _setupConfigSection(self): self.psdConfigWidget = DistributionsForm(hasSatCorrection=True, hasResolutionConfig=True) self.hpsdConfigWidget = DistributionsForm(hasSatCorrection=True, hasResolutionConfig=False) self.distribWidget = DirectedDistributionForm() + self.pnmReportWidget = PNMReportForm() + self.pnmReportWidget.objectName = "PNMReportForm" + self.configWidget.addWidget(self.pnmReportWidget) self.configWidget.addWidget(self.psdConfigWidget) self.configWidget.addWidget(self.hpsdConfigWidget) self.configWidget.addWidget(self.distribWidget) @@ -692,6 +829,7 @@ def _setupOutput(self): self.ioFileOutputLineEdit.settingKey = "ioFileoutputMicrotom" self.outputPrefix = qt.QLineEdit() + self.outputPrefix.objectName = "Output Prefix Line Edit" self.simBtn = qt.QPushButton("Apply") self.simBtn.setStyleSheet("QPushButton {font-size: 11px; font-weight: bold; padding: 8px; margin: 4px}") @@ -700,6 +838,12 @@ def _setupOutput(self): self.simBtn.clicked.connect(self.onExecuteClicked) + self.canBtn = qt.QPushButton("Cancel") + self.canBtn.setStyleSheet("QPushButton {font-size: 11px; font-weight: bold; padding: 8px; margin: 4px}") + self.canBtn.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred) + self.canBtn.objectName = "CancelButton" + self.canBtn.clicked.connect(self.onCancelClicked) + self.dialogShowWidget = self.showThreads() self.ioResultsComboBox = qt.QHBoxLayout() @@ -718,7 +862,11 @@ def _setupOutput(self): ioPageFormLayout.addRow("Output Prefix: ", self.outputPrefix) ioPageLayout.addLayout(ioPageFormLayout) - ioPageLayout.addWidget(self.simBtn) + + hbox = qt.QHBoxLayout() + hbox.addWidget(self.simBtn) + hbox.addWidget(self.canBtn) + ioPageLayout.addLayout(hbox) ioPageLayout.addLayout(self.ioResultsComboBox) @@ -728,8 +876,66 @@ def _setupOutput(self): return ioSection + def _setupServerManager(self): + ioSection = ctk.ctkCollapsibleButton() + ioSection.text = "Server Management" + ioPageLayout = qt.QVBoxLayout(ioSection) + + self.toggleServerButton = qt.QPushButton("Start Streamlit Server") + self.toggleServerButton.setStyleSheet("") + self.toggleServerButton.objectName = "ToggleServerButton" + + self.serverStatus = qt.QLabel("Stopped") + self.serverStatus.setFixedHeight(25) + self.serverStatus.setOpenExternalLinks(True) + self.serverStatus.objectName = "ServerStatusLabel" + + if StreamlitServerManager is not None: + self.server = StreamlitServerManager(self.simOptions, self.serverStatus, self.toggleServerButton) + self.server.objectName = "Streamlit Server Manager" + else: + self.server = None + self.toggleServerButton.clicked.connect(self.onStreamlitServerUnavailable) + + ioPageFormLayout = qt.QFormLayout() + ioPageFormLayout.addRow("Streamlit Server: ", self.toggleServerButton) + ioPageFormLayout.addRow("Streamlit Server Status: ", self.serverStatus) + ioPageLayout.addLayout(ioPageFormLayout) + + ioPageLayout.addStretch(1) + + LOCK_FILE = f"{slicer.app.slicerHome}/LTrace/streamlit_server.lock" + if Path(LOCK_FILE).exists(): + try: + lock_file = open(Path(LOCK_FILE), "r") + line = lock_file.readline() + self.server.pid = int(line.split("=")[1]) + line = lock_file.readline() + self.server.ip_addr = line.split("=")[1][:-1] + lock_file.close() + + if self.server.ip_addr is not None: + self.toggleServerButton.setStyleSheet("QPushButton {color: #00FF00}") + self.toggleServerButton.text = "Stop Streamlit Server" + self.serverStatus.text = f'Running in {self.ip_addr}' + else: + slicer.util.errorDisplay( + "Error in retrieving ip address from server, try again, or check if the port 8501 is available and if has a already running instance of streamlit." + ) + self.server.killStreamlitServer() + except Exception as e: + os.remove(Path(LOCK_FILE)) + import traceback + + traceback.print_exc() + + return ioSection + + def onStreamlitServerUnavailable(self): + slicer.util.errorDisplay("Server unavailable at this version.") + def showThreads(self): - dialogWidget = qt.QDialog(slicer.util.mainWindow()) + dialogWidget = qt.QDialog(self.parent) dialogWidget.setModal(True) dialogWidget.setWindowTitle("Running Jobs") @@ -775,15 +981,56 @@ def _enableAllSwitches(self): self.execOptions[prev].setChecked(True) def _onSimulSelected(self, index): - modeWidget: widgets.SingleShotInputWidget = self.modeWidgets[self.currentMode] + self.modeSelectors[widgets.SingleShotInputWidget.MODE_NAME].setChecked(True) + + self.mode_label.hide() + for key, widget in self.modeSelectors.items(): + widget.hide() + + self.canBtn.hide() + + self.serverManager.hide() + + modeWidget = self.modeWidgets[widgets.SingleShotInputWidget.MODE_NAME] + batchWidget = self.modeWidgets[widgets.BatchInputWidget.MODE_NAME] if "darcy_kabs_foam" in self.simOptions.currentData: + self.logicType = MicrotomRemoteLogic + for widget in self.hideWhenInputIsScalar: widget.visible = False modeWidget.mainInput.setCurrentNode(None) modeWidget.mainInput.visible = False modeWidget.soiInput.enabled = True modeWidget.referenceInput.enabled = True + if "pnm" in self.simOptions.currentData: + self.serverManager.show() + + self.mode_label.show() + for key, widget in self.modeSelectors.items(): + widget.show() + self.canBtn.show() + self.logicType = PNMLogic + + for widget in self.hideWhenInputIsScalar: + widget.visible = False + modeWidget.mainInput.setCurrentNode(None) + modeWidget.mainInput.visible = False + modeWidget.soiInput.setCurrentNode(None) + modeWidget.soiLabel.visible = False + modeWidget.soiInput.visible = False + modeWidget.referenceInput.enabled = True + + batchWidget.ioBatchValTagLabel.text = "Image Suffix:" + batchWidget.ioBatchValTagPattern.text = ".nrrd" + batchWidget.ioBatchROITagLabel.visible = False + batchWidget.ioBatchSegTagLabel.visible = False + batchWidget.ioBatchLabelLabel.visible = False + batchWidget.ioBatchROITagPattern.visible = False + batchWidget.ioBatchSegTagPattern.visible = False + batchWidget.ioBatchLabelPattern.visible = False else: + self.logicType = MicrotomRemoteLogic + for widget in self.hideWhenInputIsScalar: widget.visible = True modeWidget.mainInput.visible = True @@ -791,25 +1038,28 @@ def _onSimulSelected(self, index): modeWidget.referenceInput.setCurrentNode(None) if "krel" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(6) + self.configWidget.setCurrentIndex(7) self._disableSwitchFor("Local", keep="Remote") elif "darcy" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(5) + self.configWidget.setCurrentIndex(6) self._disableSwitchFor("Local", keep="Remote") elif "kabs_rev" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(4) + self.configWidget.setCurrentIndex(5) self._disableSwitchFor("Local", keep="Remote") elif "kabs" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(3) + self.configWidget.setCurrentIndex(4) self._disableSwitchFor("Local", keep="Remote") elif "hpsd" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(1) + self.configWidget.setCurrentIndex(2) self._enableAllSwitches() elif "psd" in self.simOptions.currentData: - self.configWidget.setCurrentIndex(0) + self.configWidget.setCurrentIndex(1) self._enableAllSwitches() + elif "pnm" in self.simOptions.currentData: + self.configWidget.setCurrentIndex(0) + self._disableSwitchFor("Remote", keep="Local") else: - self.configWidget.setCurrentIndex(2) + self.configWidget.setCurrentIndex(3) self._enableAllSwitches() def _onModeClicked(self): @@ -825,8 +1075,9 @@ def _onModeClicked(self): def _onInputSelected(self, node): self.simBtn.enabled = node is not None + self.canBtn.enabled = False - if node is None: + if node is None or isinstance(node, str): return # Get name first to avoid using reference node name @@ -837,16 +1088,6 @@ def _onInputSelected(self, node): if node is None: return - # tokens = nodeName.split("_") - # if len(tokens) < 4: - # arr = slicer.util.arrayFromVolume(node).astype(np.uint8) - # image_type = "BIN" - - # shape = ["{:04d}".format(int(d)) for d in arr.shape[::-1]] - # res = min([v for v in node.GetSpacing()]) - - # nodeName = "_".join([*tokens, image_type, *shape, "{:05d}".format(int(round(res * 1e6))) + "nm"]) - self.outputPrefix.setText(nodeName) def _onReferenceSelected(self, node): @@ -857,6 +1098,7 @@ def _onReferenceSelected(self, node): if node is None: return + self.canBtn.enabled = False nodeName = node.GetName() tokens = nodeName.split("_") if len(tokens) < 4: @@ -869,6 +1111,26 @@ def _onReferenceSelected(self, node): nodeName = "_".join([*tokens, image_type, *shape, "{:05d}".format(int(round(res * 1e6))) + "nm"]) self.outputPrefix.setText(nodeName) + elif "pnm" in self.simOptions.currentData: + self.simBtn.enabled = node is not None + self.canBtn.enabled = False + + if node is None: + return + + nodeName = node.GetName() + tokens = nodeName.split("_") + if len(tokens) < 4: + arr = slicer.util.arrayFromVolume(node).astype(np.float64) # TODO Precisa converter para array? + image_type = "PNM" + + shape = ["{:04d}".format(int(d)) for d in arr.shape[::-1]] + res = min([v for v in node.GetSpacing()]) + + nodeName = "_".join([*tokens, image_type, *shape, "{:05d}".format(int(round(res * 1e6))) + "nm"]) + + self.outputPrefix.setText(nodeName) + self.pnmReportWidget.wellName.setText(nodeName) def onSelectionChanged(self): items = self.ioFileVariablesList.selectedItems() @@ -880,7 +1142,7 @@ def onCLICancelCLicked(self): amatch = re.search(r"\[(.*)\]", text) if amatch: workspace = amatch.group(1) - print(f"Cancelling {workspace}") + logging.debug(f"Cancelling {workspace}") self.logic.cancelRemoteExecution(workspace) except Exception as e: pass @@ -925,17 +1187,27 @@ def showJobs(self): slicer.util.selectModule("JobMonitor") def onExecuteClicked(self): - try: + self._createLogic(logicCls=self.logicType) self.simBtn.enabled = False self.simBtn.blockSignals(True) + self.canBtn.enabled = True + self.canBtn.blockSignals(False) slicer.app.processEvents() - modeWidget: widgets.SingleShotInputWidget = self.modeWidgets[self.currentMode] + modeWidget = self.modeWidgets[self.currentMode] - inputNode = modeWidget.mainInput.currentNode() - roiNode = modeWidget.soiInput.currentNode() - refNode = modeWidget.referenceInput.currentNode() + if self.currentMode == widgets.BatchInputWidget.MODE_NAME: + inputNode = None + batchDir = modeWidget.ioFileInputLineEdit.currentPath + segTag = modeWidget.ioBatchSegTagPattern.text + roiTag = modeWidget.ioBatchROITagPattern.text + valTag = modeWidget.ioBatchValTagPattern.text + labelTag = modeWidget.ioBatchLabelPattern.text + else: + inputNode = modeWidget.mainInput.currentNode() + roiNode = modeWidget.soiInput.currentNode() + refNode = modeWidget.referenceInput.currentNode() outputPathStr = self.ioFileOutputLineEdit.currentPath if outputPathStr: @@ -962,38 +1234,660 @@ def onExecuteClicked(self): params = self.configWidget.currentWidget().params() - uid = self.logic.run( - self.simOptions.currentData, + if self.currentMode == widgets.BatchInputWidget.MODE_NAME: + uid = self.logic.runInBatch( + self.simOptions.currentData, + inputNode, + batchDir, + segTag, + roiTag, + valTag, + labelTag, + output_path=outputPath, + mode=self.chosenExecutionMode, + outputPrefix=self.outputPrefix.text, + params=params, + ) + else: + uid = self.logic.run( + self.simOptions.currentData, + inputNode, + refNode, + labels=labelselection, + roiNode=roiNode, + output_path=outputPath, + mode=self.chosenExecutionMode, + outputPrefix=self.outputPrefix.text, + params=params, + ) + + if self.chosenExecutionMode == "Remote" and uid: + self.showJobs() + + except Exception as e: + print(repr(e)) + + def restartApplyButton(self): + slicer.app.processEvents() + self.simBtn.blockSignals(False) + self.simBtn.enabled = True + self.canBtn.blockSignals(True) + self.canBtn.enabled = False + + def onCancelClicked(self): + self.logic.cancel() + self.restartApplyButton() + + +class MicrotomRemoteLogicBase(LTracePluginLogic): + processFinished = qt.Signal() + + def __init__(self, parent, progressBar): + LTracePluginLogic.__init__(self, parent) + self.progressBar = progressBar + self.cliNode = None + self._cliNodeObserver = None + + +# +# PNMLogic +# +class PNMQueue(qt.QObject): + simChanged = qt.Signal() + + +class PNMLogic(MicrotomRemoteLogicBase): + def __init__(self, parent, progressBar): + super().__init__(parent, None) + self.folder = None + self.params = None + + self.extractState = False + self.kabsOneAngleState = False + self.kabsMultiAngleState = False + self.sensibilityState = False + self.MICPState = False + self.finished = False + self.batchExecution = False + + self.logic_models = SubscaleLogicDict + + def set_subres_model(self, table_node, params): + pore_network = geo2spy(table_node) + x_size = float(table_node.GetAttribute("x_size")) + y_size = float(table_node.GetAttribute("y_size")) + z_size = float(table_node.GetAttribute("z_size")) + volume = x_size * y_size * z_size + + subres_model = params["subres_model_name"] + subres_params = params["subres_params"] + if (subres_model == "Throat Radius Curve" or subres_model == "Pressure Curve") and subres_params: + subres_params = { + i: np.asarray(subres_params[i]) if subres_params[i] is not None else None for i in subres_params.keys() + } + + subresolution_logic = self.logic_models[subres_model] + subresolution_function = subresolution_logic().get_capillary_pressure_function( + subres_params, pore_network, volume + ) + + return subresolution_function + + def deleteSubjectHierarchyFolder(self, folderName): + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + folderItemID = shNode.GetItemByName(folderName) + if folderItemID: + folderNode = shNode.GetItemDataNode(folderItemID) + if folderNode: + slicer.mrmlScene.RemoveNode(folderNode) + shNode.RemoveItem(folderItemID) + slicer.app.processEvents() + + def runInBatch( + self, + simulator, + inputNode, + batchDir, + segTag, + roiTag, + valTag, + labelTag, + output_path=None, + mode="Local", + outputPrefix="", + params=None, + ): + self.simulator = simulator + self.params = params + self.output_path = output_path + self.outputPrefix = outputPrefix + self.mode = mode + self.rootDir = None + self.progressBar = None + self.finished = False + self.cancelled = False + projectManager = ProjectManager() + + slicer.app.processEvents() + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + allItemIDs = vtk.vtkIdList() + shNode.GetItemChildren(shNode.GetSceneItemID(), allItemIDs, True) + + batch_images = [Path(batchDir) / file for file in os.listdir(batchDir) if file.endswith(valTag)] + for filepath in batch_images: + if filepath and os.path.isfile(filepath): + data, header = nrrd.read(filepath) + del data + if header["type"] == "int": + volume_node = slicer.util.loadLabelVolume(filepath) + else: + volume_node = slicer.util.loadVolume(filepath) + else: + logging.debug(f"Error at loading {filepath}, file not exists") + break + + params["well_name"] = Path(filepath).stem + + folderTree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + itemTreeId = folderTree.GetSceneItemID() + rootDir = folderTree.CreateFolderItem(itemTreeId, f"{params['well_name']} Report") + folderTree.CreateItem(rootDir, volume_node) + slicer.app.processEvents() + + self.finished = False + + self.run( + simulator, inputNode, - refNode, - labels=labelselection, - roiNode=roiNode, - output_path=outputPath, - mode=self.chosenExecutionMode, - outputPrefix=self.outputPrefix.text, + volume_node, + labels=None, + roiNode=None, + output_path=output_path, + mode=mode, + outputPrefix=params["well_name"], params=params, + isBatch=True, ) - if self.chosenExecutionMode == "Remote" and uid: - self.showJobs() + while self.finished is False: + time.sleep(0.2) + slicer.app.processEvents() + if self.cancelled is True: + break + + self.deleteSubjectHierarchyFolder(f"{params['well_name']} Report") + + self.processFinished.emit() + slicer.app.processEvents() + + def run( + self, + simulator, + segmentationNode, + referenceNode, + labels, + roiNode=None, + output_path=None, + mode="Local", + outputPrefix="", + params=None, + isBatch=False, + ): + if referenceNode is None: + return + + self.referenceNode = referenceNode + self.params = params + self.outputPrefix = outputPrefix + self.json_entry_node_ids = {} + self.pnm_report = { + "well": None, + "porosity": None, + "permeability": None, + "residual_So": None, + "realistic_production": None, + } + if self.progressBar is None: + self.progressBar = ProgressBarProc() + self.cancelled = False + + self.pnm_report["well"] = params["well_name"] + + self.json_entry_node_ids["volume"] = referenceNode.GetID() + if referenceNode.GetClassName() == "vtkMRMLLabelMapVolumeNode": + self.pnm_report["porosity"] = (slicer.util.arrayFromVolume(referenceNode) > 0).mean() + else: + self.pnm_report["porosity"] = slicer.util.arrayFromVolume(referenceNode).mean() + + local_progress_bar = LocalProgressBar() + self.extractor_logic = PoreNetworkExtractorLogic(local_progress_bar) + self.one_phase_logic = OnePhaseSimulationLogic(local_progress_bar) + self.two_phase_logic = TwoPhaseSimulationLogic(local_progress_bar) + self.micp_logic = MercurySimulationLogic(local_progress_bar) + + # Set subresolution model + if "subscale_model_params" in params: + self.subresolution_function = lambda node: self.set_subres_model(node, params["subscale_model_params"]) + self.subres_model_name = params["subscale_model_params"]["subres_model_name"] + self.subres_params = params["subscale_model_params"]["subres_params"] + else: + kabs_params = OnePhaseSimulationWidget().getParams() + self.subresolution_function = kabs_params["subresolution function call"] + self.subres_model_name = kabs_params["subres_model_name"] + self.subres_params = kabs_params["subres_params"] + + # Queue with simulations + self.sim_index = 0 + self.sim_queue = { + "extraction": self.run_extract, + "one-phase sim w/ one angle": self.run_1phase_one_angle, + "one-phase sim w/ multi angle": self.run_1phase_multi_angle, + "MICP sim": self.run_micp, + } + if params.get("sensibility_parameters_node") is not None: + self.sim_queue.update({"sensibility test simulations": self.run_sensibility}) + + self.batchExecution = isBatch + + self.controlSims = PNMQueue() + self.controlSims.simChanged.connect(self.run_next_simulation) + self.controlSims.simChanged.emit() + + def cancel(self): + if self.progressBar: + self.progressBar.nextStep(99, f"Stopping simulation on {self.referenceNode.GetName()}") + self.extractor_logic.cancel() + self.one_phase_logic.cancel() + self.two_phase_logic.cancel() + self.micp_logic.cancel() + + self.cancelled = True + self.finished = True + if self.progressBar: + self.progressBar.nextStep(100, "Cancelled") + self.progressBar.__exit__(None, None, None) + self.progressBar = None + + def run_next_simulation(self): + if self.cancelled: + return + + sim_keys = list(self.sim_queue.keys()) + sim_list = list(self.sim_queue.values()) + + progressStep = self.sim_index * 100.0 / len(self.sim_queue) + self.progressBar.nextStep(progressStep, f"Running {sim_keys[self.sim_index]} on {self.referenceNode.GetName()}") + + sim_list[self.sim_index]() + self.sim_index += 1 + + def run_extract(self): + self.extractor_logic.extract( + self.referenceNode, + None, + self.outputPrefix, + "PoreSpy", + self.extract_callback(self.extractor_logic), + ) + + def extract_callback(self, logic): + def onFinishExtract(state): + if state: + if logic.results: + self.pore_table = logic.results["pore_table"] + self.throat_table = logic.results["throat_table"] + else: + logging.debug("No connected network was identified. Possible cause: unsegmented pore space.") + return + + self.json_entry_node_ids["pore_table"] = self.pore_table.GetID() + self.json_entry_node_ids["throat_table"] = self.throat_table.GetID() + + model_nodes = logic.results["model_nodes"] + for i, node in enumerate(model_nodes["pores_nodes"]): + self.json_entry_node_ids[f"pore_polydata_{i}"] = node.GetID() + for i, node in enumerate(model_nodes["throats_nodes"]): + self.json_entry_node_ids[f"throat_polydata_{i}"] = node.GetID() + + folderTree = slicer.mrmlScene.GetSubjectHierarchyNode() + itemTreeId = folderTree.GetItemByDataNode(self.pore_table) + parentItemId = folderTree.GetItemParent(itemTreeId) + folderTree.SetItemExpanded(parentItemId, False) + + self.extractState = True + + slicer.app.processEvents() + self.checkFinish() + + return onFinishExtract + + # Kabs One-angle + def run_1phase_one_angle(self): + kabs_params = OnePhaseSimulationWidget().getParams() + kabs_params["keep_temporary"] = True + kabs_params["subresolution function call"] = self.subresolution_function + kabs_params["subresolution function"] = kabs_params["subresolution function call"](self.pore_table) + kabs_params["subres_model_name"] = self.subres_model_name + kabs_params["subres_params"] = self.subres_params + try: + self.one_phase_logic.run_1phase( + self.pore_table, + kabs_params, + prefix=self.outputPrefix, + callback=self.kabs_oneangle_callback(self.one_phase_logic), + ) + except Exception as error: + logging.error("Error occured in one-phase one-angle simulation.") + import traceback + + traceback.print_exc() + + def kabs_oneangle_callback(self, logic): + def onFinishKabs(state): + if state: + if "flow_rate" in logic.results: + flow_rate_node = slicer.util.getNode(logic.results["flow_rate"]) + self.json_entry_node_ids["flow_rate"] = flow_rate_node.GetID() + + if "permeability" in logic.results: + perm_node = slicer.util.getNode(logic.results["permeability"]) + self.json_entry_node_ids["perm_node"] = perm_node.GetID() + + perm_df = slicer.util.dataframeFromTable(perm_node) + self.pnm_report["permeability"] = np.diag(perm_df).mean() + + self.kabsOneAngleState = True + + slicer.app.processEvents() + self.checkFinish() + + return onFinishKabs + + # Kabs Multi-angle + def run_1phase_multi_angle(self): + kabs_params = OnePhaseSimulationWidget().getParams() + kabs_params["simulation type"] = "Multiple orientations" + kabs_params["rotation angles"] = 100 + kabs_params["keep_temporary"] = True + kabs_params["subresolution function call"] = self.subresolution_function + kabs_params["subresolution function"] = kabs_params["subresolution function call"](self.pore_table) + kabs_params["subres_model_name"] = self.subres_model_name + kabs_params["subres_params"] = self.subres_params + try: + self.one_phase_logic.run_1phase( + self.pore_table, + kabs_params, + prefix=self.outputPrefix, + callback=self.kabs_multiangle_callback(self.one_phase_logic), + ) except Exception as e: - print_debug(repr(e)) - finally: + logging.error("Error occured in one-phase multi-angle simulation.") + import traceback + + traceback.print_exc() + + def kabs_multiangle_callback(self, logic): + def onFinishKabs(state): + if state: + if all(v in logic.results for v in ["model", "arrow", "plane", "sphere"]): + model_node = slicer.util.getNode(logic.results["model"]) + arrow_node = slicer.util.getNode(logic.results["arrow"]) + plane_node = slicer.util.getNode(logic.results["plane"]) + sphere_node = slicer.util.getNode(logic.results["sphere"]) + else: + return + + self.json_entry_node_ids["multiangle_model"] = model_node.GetID() + self.json_entry_node_ids["multiangle_arrow_model"] = arrow_node.GetID() + self.json_entry_node_ids["multiangle_plane_model"] = plane_node.GetID() + self.json_entry_node_ids["multiangle_sphere_model"] = sphere_node.GetID() + + # measure angles + plane_node = slicer.util.getNode(logic.results["plane"]) + plane_points = plane_node.GetPolyData().GetPoints() + plane_v1 = np.array(plane_points.GetPoint(1)) - np.array(plane_points.GetPoint(0)) + plane_v2 = np.array(plane_points.GetPoint(2)) - np.array(plane_points.GetPoint(0)) + plane_normal = np.cross(plane_v2, plane_v1) + + direction = logic.results["direction"] + angle_with_plane = np.pi / 2.0 - np.arccos( + np.dot(direction, plane_normal) / (np.linalg.norm(direction) * np.linalg.norm(plane_normal)) + ) + + projection = direction - np.dot(direction, plane_normal) / np.linalg.norm(plane_normal) + projection_angle_with_z = np.arccos( + np.dot(projection, np.array([0, 0, 1])) / np.linalg.norm(projection) + ) + + # measure min, max, mean, desvio padrão dos valores + permeabilities = logic.results["permeabilities"] + perm_stats = pd.DataFrame(permeabilities[:, 3]).describe() + + df = pd.DataFrame( + { + "Angle with plane (º)": angle_with_plane * 180 / np.pi, + "Projection angle with z-axis (º)": projection_angle_with_z * 180 / np.pi, + "Average Permeability (mD)": 1000 * perm_stats.loc["mean"].tolist()[0], + "Standard Deviation Permeability (mD)": 1000 * perm_stats.loc["std"].tolist()[0], + "Min. Permeability (mD)": 1000 * perm_stats.loc["min"].tolist()[0], + "Max. Permeability (mD)": 1000 * perm_stats.loc["max"].tolist()[0], + }, + index=[0], + ) + table = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + table.SetName("multiangle_statistics") + du.dataFrameToTableNode(df, table) + folderTree = slicer.mrmlScene.GetSubjectHierarchyNode() + itemTreeId = folderTree.GetItemByDataNode(self.referenceNode) + parentItemId = folderTree.GetItemParent(itemTreeId) + folderTree.CreateItem(parentItemId, table) + self.json_entry_node_ids["multiangle_statistics"] = table.GetID() + + self.kabsMultiAngleState = True + + slicer.app.processEvents() + self.checkFinish() + + return onFinishKabs + + # Sensibilidade + def run_sensibility(self): + twoPhaseWidget = TwoPhaseSimulationWidget() + twoPhaseWidget.parameterInputWidget.setCurrentNode(self.params["sensibility_parameters_node"]) + twoPhaseWidget.onParameterInputLoad() + krel_params = twoPhaseWidget.getParams() + krel_params["subresolution function call"] = self.subresolution_function + krel_params["subresolution function"] = krel_params["subresolution function call"](self.pore_table) + krel_params["subres_model_name"] = self.subres_model_name + krel_params["subres_params"] = self.subres_params + try: + self.two_phase_logic.run_2phase( + self.pore_table, + krel_params, + prefix=self.outputPrefix, + callback=self.sensibility_callback(self.two_phase_logic), + ) + except Exception as e: + logging.debug("Error occured in two-phase sensibility simulation.") + import traceback + + traceback.print_exc() + + def sensibility_callback(self, logic): + def onFinishKrel(state): + if state: + try: + krelResultsTableNode = slicer.util.getNode(logic.krelResultsTableNodeId) + krelResultsTableNode.SetName("Sensibility") + self.json_entry_node_ids["sensibility"] = krelResultsTableNode.GetID() + + krel_df = slicer.util.dataframeFromTable(krelResultsTableNode) + swr = krel_df["result-swr"] + self.pnm_report["residual_So"] = np.median(1 - swr) + + for i in range(3): + krelCycleTableNode = slicer.util.getNode(logic.krelCycleTableNodesId[i]) + krelCycleTableNode.SetName(f"Sensibility cycle {i}") + self.json_entry_node_ids[f"sensibility_cycle{i}"] = krelCycleTableNode.GetID() + + # Production + pnm_production_logic = PoreNetworkProductionLogic() + water_viscosity = 0.001 + oil_viscosity = 0.01 + krel_smoothing = 2.0 + simulation = 0 + sensibility = True + production_table = pnm_production_logic.run( + krelResultsTableNode, + water_viscosity, + oil_viscosity, + krel_smoothing, + sensibility, + simulation, + ) + self.json_entry_node_ids["production"] = production_table.GetID() + + npd_points_vtk_array = production_table.GetTable().GetColumnByName("realistic_NpD") + npd_points = vtk.util.numpy_support.vtk_to_numpy(npd_points_vtk_array) + self.pnm_report["realistic_production"] = np.median(npd_points) + + self.sensibilityState = True + except Exception as e: + self.json_entry_node_ids["sensibility"] = None + self.json_entry_node_ids["production"] = None + for i in range(3): + self.json_entry_node_ids[f"sensibility_cycle{i}"] = None + + self.pnm_report["residual_So"] = None + self.pnm_report["realistic_production"] = None + + logging.error("Error on sensibility callback.") + import traceback + + traceback.print_exc() + + slicer.app.processEvents() + self.checkFinish() + + return onFinishKrel + + # MICP + def run_micp(self): + micp_params = MercurySimulationWidget().getParams() + micp_params["subresolution function call"] = self.subresolution_function + micp_params["subresolution function"] = micp_params["subresolution function call"](self.pore_table) + micp_params["subres_model_name"] = self.subres_model_name + micp_params["subres_params"] = self.subres_params + try: + self.micp_logic.run_mercury( + self.pore_table, + micp_params, + prefix=self.outputPrefix, + callback=self.micp_callback(self.micp_logic), + ) + except Exception as e: + logging.error("Error occured in micp simulation.") + import traceback + + traceback.print_exc() + + def micp_callback(self, logic): + def onFinishMICP(state): + if state: + micp_results_node_id = logic.results_node_id + if micp_results_node_id: + micp_results = slicer.util.getNode(micp_results_node_id) + self.json_entry_node_ids["micp"] = micp_results.GetID() + + self.MICPState = True + + slicer.app.processEvents() + self.checkFinish() + + return onFinishMICP + + # Check for finish or send another simulation in queue + def checkFinish(self): + if self.sim_index < len(self.sim_queue): + self.controlSims.simChanged.emit() + else: + if not self.referenceNode: + return + + self.progressBar.nextStep(99, f"Saving report") + + folder = Path(slicer.app.slicerHome) / "LTrace" / "stprojects" + folder.mkdir(parents=True, exist_ok=True) + + projects_path = folder / "projects.json" + pnm_report_path = folder / "folder_report.csv" + + pnm_report_df = pd.DataFrame(self.pnm_report, index=[0]) + + if pnm_report_path.exists(): + existing_df = pd.read_csv(pnm_report_path, index_col=0) + updated_df = pd.concat([existing_df, pnm_report_df], ignore_index=True) + else: + updated_df = pnm_report_df + + updated_df.index.name = "index" + updated_df.to_csv(pnm_report_path, index=True, mode="w") + + if not projects_path.exists(): + with open(projects_path, "w") as f: + f.write("") + + with open(projects_path, "r") as f: + projects_dict = json.load(f) if projects_path.stat().st_size != 0 else {} + + if (folder / self.outputPrefix).exists(): + shutil.rmtree(folder / self.outputPrefix) + + os.mkdir(folder / self.outputPrefix) + + self.json_entry = {} + for key, node_id in self.json_entry_node_ids.items(): + node = slicer.mrmlScene.GetNodeByID(node_id) if node_id else None + if node is None: + continue + + if isinstance(node, slicer.vtkMRMLScalarVolumeNode) or isinstance( + node, slicer.vtkMRMLLabelMapVolumeNode + ): + name = f"{folder}/{self.outputPrefix}/{key}.nrrd" + elif isinstance(node, slicer.vtkMRMLTableNode): + name = f"{folder}/{self.outputPrefix}/{key}.tsv" + elif isinstance(node, slicer.vtkMRMLModelNode): + name = f"{folder}/{self.outputPrefix}/{key}.vtk" + + slicer.util.saveNode(node, name) + + self.json_entry[key] = os.path.basename(name) if node else None + + projects_dict[self.outputPrefix] = self.json_entry + + with open(projects_path, "w") as f: + json.dump(projects_dict, f) + + self.finished = True + + self.progressBar.nextStep(100, "Completed") + self.progressBar.__exit__(None, None, None) + self.progressBar = None + + if not self.batchExecution: + self.processFinished.emit() + slicer.app.processEvents() - self.simBtn.blockSignals(False) - self.simBtn.enabled = True # # MicrotomRemoteLogic # -class MicrotomRemoteLogic(LTracePluginLogic): - def __init__(self, parent, progressBar): - LTracePluginLogic.__init__(self, parent) - self.progressBar = progressBar - self.cliNode = None + +class MicrotomRemoteLogic(MicrotomRemoteLogicBase): def loadDataset(self, ds, key, name, refNode=None): ds_ref = ds[key] volumeArray = ds_ref.data @@ -1079,10 +1973,6 @@ def addNodesToScene(self, nodes): helpers.makeTemporaryNodePermanent(node, show=True) node.GetDisplayNode().SetVisibility(True) - # name_template = "_".join([v for v in (prefix, simulator, dtype) if v]) - # nodeName = slicer.mrmlScene.GenerateUniqueName(name_template) - # node.SetName(nodeName) - except Exception as e: slicer.util.errorDisplay(f"Failed to load the results.") logging.error(f"ERROR :: Cause: {repr(e)}") @@ -1179,33 +2069,6 @@ def _writeOutputs(self, jsonDict, nth_run): except Exception as e: slicer.util.errorDisplay(f"Failed to load output table. Cause: {repr(e)}") - # if simulator == "krel": - # workspaceDir = None - # for vtkVolumePath, volumeName in zip( - # jsonDict.get("output_vtk_volumes", ""), jsonDict.get("output_volumes_titles", "") - # ): - # try: - # properties = {"name": volumeName} - # vtkVolumePath = str(Path(vtkVolumePath)) - # workspaceDir = Path(vtkVolumePath).parents[2] - # volume = slicer.util.loadVolume(vtkVolumePath, properties=properties) - # folderTree.CreateItem(foundResultDir, volume) - # except Exception as e: - # slicer.util.errorDisplay(f"Failed to load vtk simulation output volume {volumeName}:\n {repr(e)}") - - # for tablePath, tableName in zip( - # jsonDict.get("output_tables", ""), jsonDict.get("output_tables_titles", "") - # ): - # try: - # tablePath = str(Path(tablePath)) - # table = slicer.util.loadTable(tablePath) - # table.SetName(tableName) - # folderTree.CreateItem(foundResultDir, table) - # except Exception as e: - # slicer.util.errorDisplay(f"Failed to load permeability table {tableName}:\n {repr(e)}") - # if workspaceDir is not None: - # shutil.rmtree(workspaceDir) - def findSequenceAndAddNode(self, name, nodes, sequenceIndex): browserNode = helpers.tryGetNode(name.replace("_Proxy", "_Browser")) if not browserNode: @@ -1283,7 +2146,7 @@ def loadCustomLog(self, path: Path): lines = f.readlines() data = defaultdict(list) - for line in lines[:MAX_LOOP_ITERATIONS]: + for line in lines: cols = line.strip().split(",") for c in cols: m = pat.search(c.strip()) @@ -1293,18 +2156,16 @@ def loadCustomLog(self, path: Path): return pd.DataFrame(data) except Exception as e: - print_stack() - print_debug( - f"Failed to load custom log file. Path: {path.as_posix()}. Cause: {repr(e)}.", - channel=logging.warning, - ) + import traceback + + traceback.print_exc() + print(repr(e)) return None def onPorosimetryCLIModified(self, sim_info, cliNode, event): sequenceIndex = -1 if cliNode is None: - del self.cliNode self.cliNode = None return @@ -1354,9 +2215,11 @@ def onPorosimetryCLIModified(self, sim_info, cliNode, event): self.findSequenceAndAddNode(prefix, nodes, sequenceIndex) if not cliNode.IsBusy(): - print("ExecCmd CLI %s" % cliNode.GetStatusString()) + logging.info("ExecCmd CLI %s" % cliNode.GetStatusString()) + del self.cliNode self.cliNode = None + self.processFinished.emit() if sequenceIndex < 1: try: @@ -1418,9 +2281,6 @@ def runLocal( You can catch the cliNode and handle events, like messages, progress, errors and flow controls like Cancel. """ - if self.cliNode: - del self.cliNode - self.cliNode = slicer.cli.run(slicer.modules.porosimetrycli, None, cliParams, wait_for_completion=False) self.cliNode.AddObserver("ModifiedEvent", partial(self.onPorosimetryCLIModified, sim_info)) # Setup progress bar @@ -1489,7 +2349,9 @@ def run( return uid except Exception as e: - print_stack() + import traceback + + traceback.print_exc() helpers.removeTemporaryNodes(environment=tag) slicer.util.errorDisplay("Sorry, something went wrong...check out the logs") @@ -1573,8 +2435,6 @@ def dispatch(self, simulator, labelmapVolumeNode, outputPrefix, tag: Tag = None, post_args=dict(vfrac=vfrac), ) - print("CLI Run") - job_name = f"{simulator}: {outputPrefix} ({direction})" return slicer.modules.RemoteServiceInstance.cli.run(managed_cmd, name=job_name, job_type="microtom") diff --git a/src/modules/MicrotomRemote/PorosimetryCLI/PorosimetryCLI.py b/src/modules/MicrotomRemote/PorosimetryCLI/PorosimetryCLI.py index d0249ad..e766c4f 100644 --- a/src/modules/MicrotomRemote/PorosimetryCLI/PorosimetryCLI.py +++ b/src/modules/MicrotomRemote/PorosimetryCLI/PorosimetryCLI.py @@ -9,15 +9,12 @@ import vtk, slicer, slicer.util, mrml -import microtom import numpy as np import pandas as pd -import re -from ltrace.algorithms.common import FlowSetter from ltrace.slicer.cli_utils import progressUpdate, readFrom, writeDataInto -from ltrace.wrappers import sanitize_file_path, filter_module_name -from pathvalidate.argparse import sanitize_filepath_arg + +import microtom """ CLI Core functionality @@ -90,8 +87,7 @@ def main(args): processInfo = {"simulator": args.simulator, "workspace": args.workspace} - returnParameterFile = sanitize_file_path(args.returnparameterfile) - with open(returnParameterFile.as_posix(), "w") as returnFile: + with open(args.returnparameterfile, "w") as returnFile: returnFile.write("porosimetry=" + json.dumps(processInfo) + "\n") progressUpdate(value=100 / 100.0) @@ -101,38 +97,19 @@ def main(args): import argparse parser = argparse.ArgumentParser(description="LTrace Image Compute Wrapper for Slicer.") - parser.add_argument( - "--master", type=sanitize_filepath_arg, dest="inputVolume", required=True, help="Intensity Input (3d) Values" - ) - parser.add_argument( - "--output", - type=sanitize_filepath_arg, - dest="outputVolume", - default=None, - help="PSD Output labelmap (3d) Values", - ) + parser.add_argument("--master", type=str, dest="inputVolume", required=True, help="Intensity Input (3d) Values") + parser.add_argument("--output", type=str, dest="outputVolume", default=None, help="PSD Output labelmap (3d) Values") parser.add_argument("--psdaxis", type=str, dest="psdAxis", default=None, help="PSD Output Axis") - parser.add_argument("--outputdir", type=sanitize_filepath_arg, required=True, help="Output location to save") + parser.add_argument("--outputdir", type=str, required=True, help="Output location to save") parser.add_argument("--params", type=str, default=None, help="Simulator configuration") parser.add_argument("--simulator", type=str, required=True, help="Simulator to be executed") parser.add_argument("--workspace", type=str, required=True, help="Workspace to run") # This argument is automatically provided by Slicer channels, just capture it when using argparse parser.add_argument( - "--returnparameterfile", - type=sanitize_filepath_arg, - default=None, - help="File destination to store an execution outputs", + "--returnparameterfile", type=str, default=None, help="File destination to store an execution outputs" ) args = parser.parse_args() - args.inputVolume = sanitize_file_path(args.inputVolume) - if args.outputVolume: - args.outputVolume = sanitize_file_path(args.outputVolume) - args.outputdir = sanitize_file_path(args.outputdir) - args.simulator = filter_module_name(args.simulator) - - if not hasattr(microtom, args.simulator): - raise ValueError(f"Unknown simulator: {args.simulator}") main(args) diff --git a/src/modules/MicrotomRemote/RemoteTasks/OneResultSlurm.py b/src/modules/MicrotomRemote/RemoteTasks/OneResultSlurm.py index 3d3a8ac..38f389b 100644 --- a/src/modules/MicrotomRemote/RemoteTasks/OneResultSlurm.py +++ b/src/modules/MicrotomRemote/RemoteTasks/OneResultSlurm.py @@ -14,6 +14,17 @@ from ltrace.readers.microtom.utils import parse_command_stdout, node_to_mct_format +import slicer + + +def truncate_relative_path_on(dirname: str, path: Path): + relindex = 0 + for i, part in enumerate(path.parts): + if part == dirname: + relindex = i + 1 + + return Path(*path.parts[relindex:]) + class OneResultSlurmHandler: JOB_ID_PATTERN = re.compile("job_id = ([a-zA-Z0-9]+)") @@ -88,7 +99,7 @@ def retrieve_jobinfo_from_file(self, client: Any, deploy_path: Path): f"job_id = {jobid}", f"final_results = {deploy_path}/atena_{jobid}/atena_{jobid}.nc", ] - print("output ->", output) + return output def __call__(self, caller: JobManager, uid: str, action: str, **kwargs): @@ -219,7 +230,7 @@ def progress(self, caller: JobManager, uid: str, client: Any = None): except RuntimeError as e: self.retries += 1 - if self.retries > self.MAX_RETRIES: + if self.retries < self.MAX_RETRIES: caller.set_state( uid, "PENDING", @@ -229,6 +240,15 @@ def progress(self, caller: JobManager, uid: str, client: Any = None): traceback=repr(e), ) caller.schedule(uid, "PROGRESS") + else: + caller.set_state( + uid, + "FAILED", + 0, + message="Failed to get job status. Check yout connection or account authorization.", + end_time=tsnow, + traceback=repr(e), + ) return @@ -263,14 +283,23 @@ def progress(self, caller: JobManager, uid: str, client: Any = None): # TODO change condition to subprocess is a spawner check if self.simulator == "krel": - self.subprocess_results(self.local_dir / uid / self.simulator) + results = self.subprocess_results(self.local_dir / uid / self.simulator) else: job = caller.jobs[uid] sim_info = job.details + results = [] for result_location in sim_info["final_results"]: remote_path = PurePosixPath(result_location) - local_file: Path = self.local_dir / uid / remote_path.name - self.results.append(local_file) + target = truncate_relative_path_on(uid, remote_path) + local_file: Path = self.local_dir / uid / target + results.append(local_file) + + self.results = [] + seen = set([]) + for r in results: + if r not in seen: + self.results.append(r) + seen.add(r) if self.confirm_results(strict=self.is_strict): """job finished and got out of queue""" @@ -346,10 +375,14 @@ def collect(self, caller: JobManager, uid: str, client: Any = None): "results": self.results, **self.post_args, } - + print(sim_info) self.collector(sim_info) - self.cleanup(caller, uid, client) + slicer.util.selectModule("MicroCTEnv") + slicer.modules.MicroCTEnvWidget.mainTab.setCurrentIndex(0) + slicer.modules.MicroCTEnvWidget.mainTab.widget(0).setCurrentIndex(0) + + # self.cleanup(caller, uid, client) def confirm_results(self, strict=True): done = [] @@ -361,20 +394,23 @@ def confirm_results(self, strict=True): def subprocess_results(self, workdir: Path): import os + results = [] for sim_dir in workdir.iterdir(): try: k_table = sim_dir / "permeability.csv" last_blue = sorted(sim_dir.glob("blue*.vtk"), key=os.path.getmtime, reverse=True)[0] last_red = sorted(sim_dir.glob("red*.vtk"), key=os.path.getmtime, reverse=True)[0] - self.results.append(k_table) - self.results.append(last_blue) - self.results.append(last_red) + results.append(k_table) + results.append(last_blue) + results.append(last_red) except IndexError: pass # no files found except Exception as e: logging.error(e) + return results + def get_slurm_log(self, uid): content = {} for jobid in self.jobs: diff --git a/src/modules/ModuleInstaller/ModuleInstaller.py b/src/modules/ModuleInstaller/ModuleInstaller.py index c6fa8f7..246d1c7 100644 --- a/src/modules/ModuleInstaller/ModuleInstaller.py +++ b/src/modules/ModuleInstaller/ModuleInstaller.py @@ -85,9 +85,6 @@ def setup(self): self.apply_button.clicked.connect(self.onApplyButton) - def cleanup(self): - pass - def onSelect(self): pass diff --git a/src/modules/MonaiLabelServer/MonaiLabelServer.py b/src/modules/MonaiLabelServer/MonaiLabelServer.py index c6d4dcf..438af58 100644 --- a/src/modules/MonaiLabelServer/MonaiLabelServer.py +++ b/src/modules/MonaiLabelServer/MonaiLabelServer.py @@ -178,6 +178,7 @@ def __init__(self, parent=None): lock_file = open(Path(LOCK_FILE), "r") line = lock_file.readline() self.PID = int(line.split("=")[1]) + lock_file.close() self.timer = qt.QTimer() self.timer.timeout.connect(self.CheckIfServerIsRunning) @@ -187,12 +188,8 @@ def __init__(self, parent=None): self.widget.startServerButton.setEnabled(False) self.widget.stopServerButton.setEnabled(True) self.widget.localRadioButton.setChecked(True) - except Exception as error: + except: os.remove(Path(LOCK_FILE)) - logging.debug(f"Error: {error}") - finally: - if lock_file: - lock_file.close() for job in JobManager.jobs: if JobManager.jobs[job].job_type == "monai" and JobManager.jobs[job].status == "RUNNING": @@ -293,14 +290,9 @@ def onStartLocalServer(self): print(f"Starting {self.PID}...\n") # create the lock file - try: - lock_file = open(Path(LOCK_FILE), "w") - lock_file.write(f"PID={self.PID}") - except Exception as error: - logging.debug(f"Error: {error}") - finally: - if lock_file: - lock_file.close() + lock_file = open(Path(LOCK_FILE), "w") + lock_file.write(f"PID={self.PID}") + lock_file.close() # set a timer to check if the process is still running self.timer = qt.QTimer() diff --git a/src/modules/MultiScale/MultiScale.py b/src/modules/MultiScale/MultiScale.py index 393f52d..bcc495f 100644 --- a/src/modules/MultiScale/MultiScale.py +++ b/src/modules/MultiScale/MultiScale.py @@ -85,7 +85,7 @@ def setup(self): self.trainingImageWidget.formLayout.setContentsMargins(0, 0, 0, 0) self.trainingImageWidget.mainInput.currentItemChanged.connect(self.setTrainingImageListChecked) - self.trainingImageWidget.onReferenceSelected = self.updateFinalImageWidgets + self.trainingImageWidget.onReferenceSelectedSignal.connect(self.updateFinalImageWidgets) self.trainingImageWidget.segmentListGroup[1].itemChanged.connect(lambda: self.listItemChange()) self.trainingImageWidget.autoPorosityCalcCb.stateChanged.connect(self.setTrainingImageListChecked) @@ -248,7 +248,7 @@ def setup(self): self.maskWidget.formLayout.setContentsMargins(0, 0, 0, 0) self.maskWidget.mainInput.currentItemChanged.connect(self.onMaskChange) - self.maskWidget.onReferenceSelected = self.updateFinalImageWidgets + self.maskWidget.onReferenceSelectedSignal.connect(self.updateFinalImageWidgets) self.maskWidget.segmentListGroup[1].itemChanged.connect(lambda: self.listItemChange()) inputFormLayout = qt.QFormLayout(inputSection) diff --git a/src/modules/MultiScale/MultiscaleCLI/MultiscaleCLI.py b/src/modules/MultiScale/MultiscaleCLI/MultiscaleCLI.py index d324d55..97c31d5 100644 --- a/src/modules/MultiScale/MultiscaleCLI/MultiscaleCLI.py +++ b/src/modules/MultiScale/MultiscaleCLI/MultiscaleCLI.py @@ -16,9 +16,7 @@ from multiprocessing import cpu_count from pathlib import Path -from ltrace.constants import MAX_LOOP_ITERATIONS from ltrace.slicer.cli_utils import writeDataInto, readFrom, progressUpdate -from ltrace.wrappers import sanitize_file_path import mpslib as mps from tifffile import tifffile @@ -40,26 +38,21 @@ def saveRealizationFiles(image, grid_cell_size, filePath): def MPS(args): - temporaryPath = sanitize_file_path(args.temporaryPath) - if not isinstance(args.nreal, int) or args.nreal < 1: - raise ValueError("Invalid value for number of realizations.") - - args.nreal = min(args.nreal, MAX_LOOP_ITERATIONS) - + temporaryPath = args.temporaryPath params = json.loads(args.params) if args.params is not None else {} mpslib = mps.mpslib(method="mps_genesim") - mpslib.parameter_filename = (temporaryPath / "mps.txt").as_posix() - mpslib.par["ti_fnam"] = (temporaryPath / "ti.dat").as_posix() - mpslib.par["out_folder"] = temporaryPath.as_posix() + mpslib.parameter_filename = os.path.join(temporaryPath, "mps.txt") + mpslib.par["ti_fnam"] = os.path.join(temporaryPath, "ti.dat") + mpslib.par["out_folder"] = temporaryPath mpslib.par["simulation_grid_size"] = np.array(params["finalImageSize"]) mpslib.par["grid_cell_size"] = np.array(params["finalImageResolution"]) mpslib.par["n_cond"] = args.ncond mpslib.par["n_real"] = args.nreal mpslib.par["n_max_ite"] = args.iterations mpslib.par["rseed"] = args.rseed - mpslib.par["hard_data_fnam"] = "hard.dat" - mpslib.par["mask_fnam"] = (temporaryPath / "mask.dat").as_posix() + mpslib.par["hard_data_fnam"] = os.path.join("hard.dat") + mpslib.par["mask_fnam"] = os.path.join(temporaryPath, "mask.dat") mpslib.par["colocate_dimension"] = args.colocateDimensions mpslib.par["max_search_radius"] = args.maxSearchRadius mpslib.par["distance_max"] = args.distanceMax @@ -68,8 +61,7 @@ def MPS(args): mpslib.run_parallel() for realization in range(args.nreal): - simDataFile = temporaryPath / f"sim_data_{realization}.npy" - np.save(simDataFile.as_posix(), mpslib.sim[realization]) + np.save(os.path.join(temporaryPath, f"sim_data_{realization}.npy"), mpslib.sim[realization]) if __name__ == "__main__": diff --git a/src/modules/MultiThresholdEffect/MultiThresholdEffectLib/SegmentEditorEffect.py b/src/modules/MultiThresholdEffect/MultiThresholdEffectLib/SegmentEditorEffect.py index 2a5221c..29a488b 100644 --- a/src/modules/MultiThresholdEffect/MultiThresholdEffectLib/SegmentEditorEffect.py +++ b/src/modules/MultiThresholdEffect/MultiThresholdEffectLib/SegmentEditorEffect.py @@ -51,6 +51,7 @@ def __init__(self, scriptedEffect): self.previewSteps = 7 self.timer = qt.QTimer() self.timer.connect("timeout()", self.preview) + self.timer.setParent(self.scriptedEffect.optionsFrame()) self.previewPipelines = {} self.setupPreviewDisplay() self.renderedLayout = None @@ -84,6 +85,10 @@ def helpText(self):

""" def activate(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + self.SetSourceVolumeIntensityMaskOff() hide_masking_widget(self) self.sourceVolumeNodeChanged() @@ -128,6 +133,10 @@ def deactivate(self): self.clearPreviewDisplay() self.timer.stop() + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + # Show current segmentation segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if segmentationNode: @@ -253,6 +262,10 @@ def createCursor(self, widget): return slicer.util.mainWindow().cursor def getParentLazyNode(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + node = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode() parentLazyNodeId = node.GetAttribute("ParentLazyNode") if parentLazyNodeId: @@ -262,6 +275,10 @@ def getParentLazyNode(self): return None def sourceVolumeNodeChanged(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + self.applyFullButton.visible = False if self.scriptedEffect.parameterSetNode().GetActiveEffectName() != self.scriptedEffect.name: @@ -307,6 +324,10 @@ def createHistogram(self, nparray=None, k=None): self.applyKmeans() def getColors(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return [] + self.colorsBySegment = dict() segmentIDs = vtk.vtkStringArray() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() @@ -384,6 +405,10 @@ def redrawHistogram(self, onlyBars=False): self.drawTable() def drawTable(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + self.table.blockSignals(True) segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() segmentation = segmentationNode.GetSegmentation() @@ -458,6 +483,10 @@ def regionChanged(self, segment, lr, test): self.redrawHistogram(onlyBars=True) def onApply(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + self.timer.stop() self.clearPreviewDisplay() segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() @@ -514,6 +543,10 @@ def onApply(self): self.applyFinishedCallback() def onApplyFull(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply the effect. The selected node is not valid.") + return + slicer.util.selectModule("MultipleThresholdBigImage") virtualSegWidget = slicer.modules.MultipleThresholdBigImageWidget @@ -572,6 +605,10 @@ def resetObservers(self): ) def updateGUIFromMRML(self): + if self.scriptedEffect.parameterSetNode() is None: + logging.debug("Segment editor node is not available.") + return + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() if self.segmentationNode != segmentationNode: self.segmentationNode = segmentationNode @@ -588,6 +625,10 @@ def updateGUIFromMRML(self): self._max = _max def applyKmeans(self): + if self.scriptedEffect.parameterSetNode() is None: + slicer.util.errorDisplay("Failed to apply. The selected node is invalid.") + return + from scipy.cluster.vq import kmeans2 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() diff --git a/src/modules/MulticoreExport/Resources/multicore_summary_template.html b/src/modules/MulticoreExport/Resources/multicore_summary_template.html index f7344a3..50b7de8 100644 --- a/src/modules/MulticoreExport/Resources/multicore_summary_template.html +++ b/src/modules/MulticoreExport/Resources/multicore_summary_template.html @@ -2,16 +2,9 @@ - +