From 8aec6ff2178a6be0489b94d1f7398ed2923ca7ae Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Fri, 27 Sep 2024 12:22:43 +0200 Subject: [PATCH 01/10] Fix for the issue #35. --- HISTORY.rst | 6 ++++++ .../widgets/lizard_archive_browser.py | 20 ++++++++++++++++++- .../widgets/ui/raster_download_settings.ui | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8158155..aec776e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History ======= +0.3.6 (unreleased) +------------------- + +- Fixed issue: #35 + + 0.3.5 (2024-9-12) ------------------ diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index e7da0d0..0a62502 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -1,6 +1,7 @@ # Lizard plugin for QGIS, licensed under GPLv2 or (at your option) any later version # Copyright (C) 2023 by Lutra Consulting for 3Di Water Management import os +from copy import deepcopy from math import ceil from operator import itemgetter @@ -10,6 +11,7 @@ QgsFieldProxyModel, QgsGeometry, QgsMapLayerProxyModel, + QgsPointXY, QgsProject, QgsRasterLayer, QgsRectangle, @@ -571,7 +573,7 @@ def download_raster_file(self): current_row = index.row() raster_uuid_item = self.raster_model.item(current_row, self.RASTER_UUID_COLUMN_IDX) raster_uuid = raster_uuid_item.text() - raster_instance = self.current_raster_instances[raster_uuid] + raster_instance = deepcopy(self.current_raster_instances[raster_uuid]) download_dir = download_settings_dlg.output_dir_raster.filePath() raster_name = download_settings_dlg.filename_le_raster.text() no_data = download_settings_dlg.no_data_sbox_raster.value() @@ -649,6 +651,22 @@ def download_raster_file(self): named_extent_polygons[fid, polygon_name] = polygon_wkt crop_to_polygon = download_settings_dlg.clip_to_polygon_ckb.isChecked() # Spawn raster downloading task + raster_instance_epsg = raster_instance["projection"] + raster_instance_crs = QgsCoordinateReferenceSystem.fromOgcWmsCrs(raster_instance_epsg) + if raster_instance_crs != target_crs: + raster_boundaries = [("origin_x", "origin_y"), ("upper_bound_x", "upper_bound_y")] + for x_coord_name, y_coord_name in raster_boundaries: + src_x_coord = raster_instance[x_coord_name] + src_y_coord = raster_instance[y_coord_name] + if src_x_coord is None or src_y_coord is None: + continue + src_point_geom = QgsGeometry.fromPointXY(QgsPointXY(src_x_coord, src_y_coord)) + dst_point_geom = reproject_geometry(src_point_geom, raster_instance_crs, target_crs) + dst_point = dst_point_geom.asPoint() + dst_x_coord = dst_point.x() + dst_y_coord = dst_point.y() + raster_instance[x_coord_name] = dst_x_coord + raster_instance[y_coord_name] = dst_y_coord raster_downloader = RasterDownloader( self.plugin.downloader, raster_instance, diff --git a/lizard_qgis_plugin/widgets/ui/raster_download_settings.ui b/lizard_qgis_plugin/widgets/ui/raster_download_settings.ui index 1c60a19..36c7019 100644 --- a/lizard_qgis_plugin/widgets/ui/raster_download_settings.ui +++ b/lizard_qgis_plugin/widgets/ui/raster_download_settings.ui @@ -295,6 +295,9 @@ 10 + + 5 + 1000000.000000000000000 From 7d5fd0c3091cc8e50df21514f6cc697f04d0bd36 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Fri, 27 Sep 2024 18:08:41 +0200 Subject: [PATCH 02/10] Added resolution selection for the scenario rasters download. --- HISTORY.rst | 2 +- lizard_qgis_plugin/utils.py | 28 +- .../widgets/lizard_archive_browser.py | 34 +- lizard_qgis_plugin/widgets/ui/lizard.ui | 66 +- .../widgets/ui/scenario_archive_browser.ui | 668 ------------------ lizard_qgis_plugin/workers.py | 15 +- 6 files changed, 97 insertions(+), 716 deletions(-) delete mode 100644 lizard_qgis_plugin/widgets/ui/scenario_archive_browser.ui diff --git a/HISTORY.rst b/HISTORY.rst index aec776e..d061dad 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ History 0.3.6 (unreleased) ------------------- -- Fixed issue: #35 +- Fixes/enhancements: #35, #37 0.3.5 (2024-9-12) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index fcbdf79..fa604d6 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -16,6 +16,7 @@ QgsGeometry, QgsLayerTreeGroup, QgsLayerTreeLayer, + QgsPointXY, QgsProject, QgsVectorFileWriter, QgsVectorLayer, @@ -194,7 +195,7 @@ def try_to_write(working_dir): os.remove(test_file_path) -def split_scenario_extent(scenario_instance, max_pixel_count=1 * 10**8): +def split_scenario_extent(scenario_instance, resolution=None, max_pixel_count=1 * 10**8): """ Split raster task spatial bounds to fit in to maximum pixel count limit. Reimplemented code from https://github.com/nens/threedi-scenario-downloader @@ -203,8 +204,12 @@ def split_scenario_extent(scenario_instance, max_pixel_count=1 * 10**8): y1 = scenario_instance["origin_y"] x2 = scenario_instance["upper_bound_x"] y2 = scenario_instance["upper_bound_y"] - pixelsize_x = abs(scenario_instance["pixelsize_x"]) - pixelsize_y = abs(scenario_instance["pixelsize_y"]) + if resolution is None: + pixelsize_x = scenario_instance["pixelsize_x"] + pixelsize_y = scenario_instance["pixelsize_y"] + else: + pixelsize_x = resolution + pixelsize_y = resolution width = abs((x2 - x1) / pixelsize_x) height = abs((y2 - y1) / pixelsize_y) if not width.is_integer(): @@ -355,6 +360,23 @@ def reproject_geometry(geometry, src_crs, dst_crs, transformation=None): return geometry +def unify_spatial_boundaries(dataset_instance, source_crs, destination_crs): + """Unify spatial boundaries of derived dataset instance (scenario or raster).""" + dataset_boundaries = [("origin_x", "origin_y"), ("upper_bound_x", "upper_bound_y")] + for x_coord_name, y_coord_name in dataset_boundaries: + src_x_coord = dataset_instance[x_coord_name] + src_y_coord = dataset_instance[y_coord_name] + if src_x_coord is None or src_y_coord is None: + continue + src_point_geom = QgsGeometry.fromPointXY(QgsPointXY(src_x_coord, src_y_coord)) + dst_point_geom = reproject_geometry(src_point_geom, source_crs, destination_crs) + dst_point = dst_point_geom.asPoint() + dst_x_coord = dst_point.x() + dst_y_coord = dst_point.y() + dataset_instance[x_coord_name] = dst_x_coord + dataset_instance[y_coord_name] = dst_y_coord + + def wkt_polygon_layer(polygon_wkt, polygon_layer_name="clip_layer", epsg="EPSG:4326"): """Spawn (multi)polygon layer out of single WKT polygon geometry.""" geometry_type = "MultiPolygon" if polygon_wkt.lower().startswith("multi") else "Polygon" diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 0a62502..3f9de37 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -11,7 +11,6 @@ QgsFieldProxyModel, QgsGeometry, QgsMapLayerProxyModel, - QgsPointXY, QgsProject, QgsRasterLayer, QgsRectangle, @@ -35,6 +34,7 @@ get_url_raster_instance, reproject_geometry, try_to_write, + unify_spatial_boundaries, ) from lizard_qgis_plugin.workers import RasterDownloader, ScenarioItemsDownloader @@ -341,6 +341,8 @@ def fetch_results(self): self.pb_download.setEnabled(True) self.grp_raster_settings.setEnabled(True) self.toggle_selection_ckb.setEnabled(True) + raster_resolution = scenario_instance["pixelsize_x"] + self.pixel_size_sbox.setValue(raster_resolution if raster_resolution else RASTER_FALLBACK_RESOLUTION) scenario_crs = QgsCoordinateReferenceSystem.fromOgcWmsCrs(scenario_instance["projection"]) self.crs_widget.setCrs(scenario_crs) @@ -385,7 +387,7 @@ def download_results(self): current_row = index.row() scenario_uuid_item = self.scenario_model.item(current_row, self.SCENARIO_UUID_COLUMN_IDX) scenario_uuid = scenario_uuid_item.text() - scenario_instance = self.current_scenario_instances[scenario_uuid] + scenario_instance = deepcopy(self.current_scenario_instances[scenario_uuid]) download_dir = self.discover_download_directory(scenario_instance) if not download_dir: self.plugin.communication.bar_info("Downloading results files canceled..") @@ -403,20 +405,27 @@ def download_results(self): else: raw_results_to_download.append(result_copy) scenario_name = scenario_instance["name"] - projection = self.crs_widget.crs().authid() no_data = self.no_data_sbox.value() + resolution = self.pixel_size_sbox.value() + target_crs = self.crs_widget.crs().authid() if not rasters_to_download and not raw_results_to_download: warn_message = "No items checked - please select items to download and try again." self.log_feedback(warn_message, Qgis.Warning) return + # Adjust scenario instance spatial boundaries to the selected CRS (if necessary) + scenario_instance_epsg = scenario_instance["projection"] + scenario_instance_crs = QgsCoordinateReferenceSystem.fromOgcWmsCrs(scenario_instance_epsg) + if scenario_instance_crs != target_crs: + unify_spatial_boundaries(scenario_instance, scenario_instance_crs, target_crs) scenario_items_downloader = ScenarioItemsDownloader( self.plugin.downloader, scenario_instance, raw_results_to_download, rasters_to_download, download_dir, - projection, no_data, + resolution, + target_crs, ) scenario_items_downloader.signals.download_progress.connect(self.on_download_progress) scenario_items_downloader.signals.download_finished.connect(self.on_download_finished) @@ -650,23 +659,12 @@ def download_raster_file(self): polygon_wkt = reproject_geometry(feat.geometry(), polygon_layer_crs, target_crs).asWkt() named_extent_polygons[fid, polygon_name] = polygon_wkt crop_to_polygon = download_settings_dlg.clip_to_polygon_ckb.isChecked() - # Spawn raster downloading task + # Adjust raster instance spatial boundaries to the selected CRS (if necessary) raster_instance_epsg = raster_instance["projection"] raster_instance_crs = QgsCoordinateReferenceSystem.fromOgcWmsCrs(raster_instance_epsg) if raster_instance_crs != target_crs: - raster_boundaries = [("origin_x", "origin_y"), ("upper_bound_x", "upper_bound_y")] - for x_coord_name, y_coord_name in raster_boundaries: - src_x_coord = raster_instance[x_coord_name] - src_y_coord = raster_instance[y_coord_name] - if src_x_coord is None or src_y_coord is None: - continue - src_point_geom = QgsGeometry.fromPointXY(QgsPointXY(src_x_coord, src_y_coord)) - dst_point_geom = reproject_geometry(src_point_geom, raster_instance_crs, target_crs) - dst_point = dst_point_geom.asPoint() - dst_x_coord = dst_point.x() - dst_y_coord = dst_point.y() - raster_instance[x_coord_name] = dst_x_coord - raster_instance[y_coord_name] = dst_y_coord + unify_spatial_boundaries(raster_instance, raster_instance_crs, target_crs) + # Spawn raster downloading task raster_downloader = RasterDownloader( self.plugin.downloader, raster_instance, diff --git a/lizard_qgis_plugin/widgets/ui/lizard.ui b/lizard_qgis_plugin/widgets/ui/lizard.ui index 0733697..36b62e5 100644 --- a/lizard_qgis_plugin/widgets/ui/lizard.ui +++ b/lizard_qgis_plugin/widgets/ui/lizard.ui @@ -540,6 +540,23 @@ Rasters download settings + + + + + Segoe UI + 10 + false + + + + Pixel size: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + @@ -554,6 +571,32 @@ + + + + + Segoe UI + 10 + + + + 5 + + + 1000000.000000000000000 + + + + + + + + Segoe UI + 10 + + + + @@ -580,16 +623,6 @@ - - - - Segoe UI - 10 - - - - - @@ -603,19 +636,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - diff --git a/lizard_qgis_plugin/widgets/ui/scenario_archive_browser.ui b/lizard_qgis_plugin/widgets/ui/scenario_archive_browser.ui deleted file mode 100644 index ac01a30..0000000 --- a/lizard_qgis_plugin/widgets/ui/scenario_archive_browser.ui +++ /dev/null @@ -1,668 +0,0 @@ - - - Dialog - - - - 0 - 0 - 1000 - 949 - - - - Lizard - - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 100 - 30 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 12 - - - - Close - - - - - - - - - 0 - - - - Simulation results - - - - - - - 0 - 0 - - - - - 500 - 300 - - - - - Segoe UI - - - - - - - false - - - - Segoe UI - 10 - - - - Rasters download settings - - - - - - - Segoe UI - 10 - false - - - - NO DATA value: - - - - - - - - Segoe UI - 10 - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - -1000000.000000000000000 - - - 1000000.000000000000000 - - - -9999.000000000000000 - - - - - - - - Segoe UI - 10 - - - - - - - - - Segoe UI - 10 - false - - - - CRS: - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - - 100 - 30 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 12 - - - - Download selected items - - - - - - - - - true - - - - 16777215 - 16777215 - - - - - Segoe UI - 10 - - - - Feedback - - - false - - - - - - - 0 - 0 - - - - - 16777215 - 100 - - - - - Segoe UI - 10 - - - - QFrame::NoFrame - - - QAbstractItemView::NoEditTriggers - - - - - - - true - - - - 100 - 30 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 12 - - - - Clear feedback - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Segoe UI - 10 - - - - QFrame::NoFrame - - - QAbstractItemView::NoEditTriggers - - - 150 - - - - - - - - Segoe UI - 10 - - - - false - - - 🔍 Search for scenario by name - - - - - - - 10 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - - 100 - 30 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 12 - - - - Show downloadable files - - - - - - - false - - - - 100 - 30 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 12 - - - - Add as WMS - - - - - - - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 400 - 20 - - - - - - - - - 0 - 0 - - - - - 40 - 20 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 10 - - - - Qt::LeftToRight - - - < - - - - - - - - 0 - 0 - - - - - 60 - 20 - - - - - Segoe UI - 10 - - - - Qt::StrongFocus - - - QSpinBox {background-color: white;} - - - false - - - Qt::AlignCenter - - - QAbstractSpinBox::NoButtons - - - / 1 - - - 1 - - - - - - - - 0 - 0 - - - - - 40 - 20 - - - - - 16777215 - 16777215 - - - - - Segoe UI - 10 - - - - > - - - - - - - - - - Segoe UI - 10 - - - - QFrame::NoFrame - - - QAbstractItemView::NoEditTriggers - - - true - - - true - - - - - - - 0 - - - - - false - - - - Segoe UI - 10 - - - - Qt::LeftToRight - - - Select/deselect all - - - - - - - - Segoe UI - 12 - - - - Scenario items - - - - - - - - - - - - - Rasters - - - - - - - - - QgsCollapsibleGroupBox - QGroupBox -
qgscollapsiblegroupbox.h
- 1 -
- - QgsProjectionSelectionWidget - QWidget -
qgsprojectionselectionwidget.h
-
-
- - -
diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index f029be5..8548de5 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -43,7 +43,15 @@ class ScenarioItemsDownloader(QRunnable): TASK_CHECK_SLEEP_TIME = 5 def __init__( - self, downloader, scenario_instance, raw_results_to_download, raster_results, download_dir, projection, no_data + self, + downloader, + scenario_instance, + raw_results_to_download, + raster_results, + download_dir, + no_data, + resolution, + projection, ): super().__init__() self.downloader = downloader @@ -56,8 +64,9 @@ def __init__( self.scenario_download_dir = os.path.join( download_dir, translate_illegal_chars(f"{self.scenario_name} ({self.scenario_simulation_id})") ) - self.projection = projection self.no_data = no_data + self.resolution = resolution + self.projection = projection self.total_progress = 100 self.current_step = 0 self.number_of_steps = 0 @@ -90,7 +99,7 @@ def download_raster_results(self): # Create tasks progress_msg = f"Spawning raster tasks and preparing for download (scenario: '{self.scenario_name}')..." self.report_progress(progress_msg) - spatial_bounds = split_scenario_extent(self.scenario_instance) + spatial_bounds = split_scenario_extent(self.scenario_instance, self.resolution) for raster_result in self.raster_results: raster_url = raster_result["raster"] lizard_url = self.downloader.LIZARD_URL From 15f08ce36ac39ad1777b76d9065b5a902604ecf9 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Tue, 8 Oct 2024 14:29:23 +0200 Subject: [PATCH 03/10] Added re-projection fix. --- lizard_qgis_plugin/widgets/lizard_archive_browser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 3f9de37..73cc399 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -407,7 +407,8 @@ def download_results(self): scenario_name = scenario_instance["name"] no_data = self.no_data_sbox.value() resolution = self.pixel_size_sbox.value() - target_crs = self.crs_widget.crs().authid() + target_crs = self.crs_widget.crs() + projection = target_crs.authid() if not rasters_to_download and not raw_results_to_download: warn_message = "No items checked - please select items to download and try again." self.log_feedback(warn_message, Qgis.Warning) @@ -425,7 +426,7 @@ def download_results(self): download_dir, no_data, resolution, - target_crs, + projection, ) scenario_items_downloader.signals.download_progress.connect(self.on_download_progress) scenario_items_downloader.signals.download_finished.connect(self.on_download_finished) From 44ed6c256f1c5dc7199ae748339c39acf5650c59 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Tue, 8 Oct 2024 14:07:42 +0200 Subject: [PATCH 04/10] Work on running Buildings Flood Risk Analysis. --- lizard_qgis_plugin/utils.py | 42 +++++++ .../widgets/lizard_archive_browser.py | 27 +++++ lizard_qgis_plugin/widgets/ui/lizard.ui | 26 +++- lizard_qgis_plugin/workers.py | 112 ++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index fa604d6..24ebad0 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -341,6 +341,48 @@ def create_raster_tasks(lizard_url, api_key, raster, spatial_bounds, projection= return raster_tasks +def upload_local_file(upload_url, local_filepath): + """Upload local file.""" + with open(local_filepath, "rb") as file: + response = requests.put(upload_url, data=file) + return response + + +def create_buildings_result(lizard_url, api_key, scenario_instance): + """Create Lizard buildings result.""" + scenario_id = scenario_instance["uuid"] + url = f"{lizard_url}scenarios/{scenario_id}/results/" + payload = {"name": "buildings", "code": "buildings", "family": "Raw"} + r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r.raise_for_status() + buildings_result = r.json() + return buildings_result + + +def create_vulnerable_buildings_result(lizard_url, api_key, scenario_instance): + """Create Lizard vulnerable buildings result.""" + scenario_id = scenario_instance["uuid"] + url = f"{lizard_url}scenarios/{scenario_id}/results/" + payload = {"name": "vulnerable_buildings", "code": "vulnerable_buildings", "family": "Vulnerable_Buildings"} + r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r.raise_for_status() + vulnerable_buildings_result = r.json() + return vulnerable_buildings_result + + +def create_buildings_flood_risk_task( + lizard_url, api_key, scenario_instance, result_id, calculation_method="dgbc", output_format="gpkg" +): + """Create Lizard buildings flood risk task.""" + scenario_id = scenario_instance["uuid"] + url = f"{lizard_url}scenarios/{scenario_id}/results/{result_id}/process/" + payload = {"method": calculation_method, "output_format": output_format} + r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r.raise_for_status() + process_task = r.json() + return process_task + + def build_vrt(output_filepath, raster_filepaths, **vrt_options): """Build VRT for the list of rasters.""" options = gdal.BuildVRTOptions(**vrt_options) diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 73cc399..18c9a19 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -379,6 +379,15 @@ def load_scenario_as_wms_layers(self): map_canvas.refresh() self.log_feedback(f"WMS layers for scenario '{scenario_name}' added to the project.") + def analyse_flood_risk_buildings(self): + index = self.scenario_tv.currentIndex() + if not index.isValid(): + return + current_row = index.row() + scenario_uuid_item = self.scenario_model.item(current_row, self.SCENARIO_UUID_COLUMN_IDX) + scenario_uuid = scenario_uuid_item.text() + # TODO: Continue here + def download_results(self): """Download selected (checked) result files.""" index = self.scenario_tv.currentIndex() @@ -462,6 +471,24 @@ def on_download_failed(self, scenario_instance, error_message): self.plugin.communication.bar_error(error_message) self.log_feedback(error_message, Qgis.Critical) + def on_flood_risk_analysis_progress(self, scenario_instance, progress_message, current_progress, total_progress): + """Feedback on flood risk analysis progress signal.""" + scenario_name = scenario_instance["name"] + msg = progress_message if progress_message else f"Processing '{scenario_name}' buildings flood risk analysis..." + self.plugin.communication.progress_bar(msg, 0, total_progress, current_progress, clear_msg_bar=True) + + def on_flood_risk_analysis_finished(self, scenario_instance, message): + """Feedback on flood risk analysis finished signal.""" + self.plugin.communication.clear_message_bar() + self.plugin.communication.bar_info(message) + self.log_feedback(message) + + def on_flood_risk_analysis_failed(self, scenario_instance, error_message): + """Feedback on flood risk analysis failed signal.""" + self.plugin.communication.clear_message_bar() + self.plugin.communication.bar_error(error_message) + self.log_feedback(error_message, Qgis.Critical) + def search_for_rasters(self): """Method used for searching rasters with text typed withing search bar.""" self.page_sbox_raster.valueChanged.disconnect(self.fetch_rasters) diff --git a/lizard_qgis_plugin/widgets/ui/lizard.ui b/lizard_qgis_plugin/widgets/ui/lizard.ui index 36b62e5..19cdcec 100644 --- a/lizard_qgis_plugin/widgets/ui/lizard.ui +++ b/lizard_qgis_plugin/widgets/ui/lizard.ui @@ -311,18 +311,40 @@ - + Qt::Horizontal - 400 + 40 20 + + + + false + + + + 100 + 25 + + + + + Segoe UI + 12 + + + + Flood risk buildings + + + diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index 8548de5..edc2981 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -14,11 +14,15 @@ from lizard_qgis_plugin.utils import ( build_vrt, clip_raster, + create_buildings_flood_risk_task, + create_buildings_result, create_raster_tasks, + create_vulnerable_buildings_result, layer_to_gpkg, split_raster_extent, split_scenario_extent, translate_illegal_chars, + upload_local_file, wkt_polygon_layer, ) @@ -29,6 +33,12 @@ class LizardDownloadError(Exception): pass +class LizardFloodRiskAnalysisError(Exception): + """Lizard flood risk analyzer exception class.""" + + pass + + class LizardDownloaderSignals(QObject): """Definition of the items download worker signals.""" @@ -37,6 +47,14 @@ class LizardDownloaderSignals(QObject): download_failed = pyqtSignal(dict, str) +class LizardFloodRiskAnalysisSignals(QObject): + """Definition of the buildings flood risk analyzer worker signals.""" + + analysis_progress = pyqtSignal(dict, str, int, int) + analysis_finished = pyqtSignal(dict, str) + analysis_failed = pyqtSignal(dict, str) + + class ScenarioItemsDownloader(QRunnable): """Worker object responsible for downloading scenario files.""" @@ -343,3 +361,97 @@ def report_failure(self, error_message): def report_finished(self, message): """Report worker finished message.""" self.signals.download_finished.emit(self.raster_instance, self.downloaded_files, message) + + +class BuildingsFloodRiskAnalyzer(QRunnable): + """Worker object responsible for running building flood risk analysis.""" + + TASK_CHECK_SLEEP_TIME = 5 + + def __init__( + self, + downloader, + scenario_instance, + buildings_gpkg, + calculation_method="dgbc", + output_format="gpkg", + ): + super().__init__() + self.downloader = downloader + self.scenario_instance = scenario_instance + self.buildings_gpkg = buildings_gpkg + self.calculation_method = calculation_method + self.output_format = output_format + self.total_progress = 100 + self.current_step = 0 + self.number_of_steps = 4 + self.percentage_per_step = self.total_progress / self.number_of_steps + self.signals = LizardFloodRiskAnalysisSignals() + + def analyze_buildings_flood_risk(self): + success_statuses = {"SUCCESS"} + in_progress_statuses = {"PENDING", "UNKNOWN", "STARTED", "RETRY"} + lizard_url = self.downloader.LIZARD_URL + api_key = self.downloader.get_api_key() + # Create a "buildings" result object for the scenario + progress_msg = f"Create a \"buildings\" result object for the scenario '{self.scenario_name}'..." + self.report_progress(progress_msg) + buildings_result = create_buildings_result(lizard_url, api_key, self.scenario_instance) + progress_msg = f"Upload buildings for the scenario '{self.scenario_name}'..." + self.report_progress(progress_msg) + buildings_result_upload_url = buildings_result["upload_url"] + upload_local_file(self.buildings_gpkg, buildings_result_upload_url) + progress_msg = f"Create a \"vulnerable buildings\" result object for the scenario '{self.scenario_name}'..." + self.report_progress(progress_msg) + vulnerable_buildings_result = create_vulnerable_buildings_result(lizard_url, api_key, self.scenario_instance) + result_id = vulnerable_buildings_result["result_id"] + progress_msg = f'Spawn processing of the "vulnerable buildings" result task...' + self.report_progress(progress_msg) + process_task = create_buildings_flood_risk_task( + lizard_url, api_key, self.scenario_instance, result_id, self.calculation_method, self.output_format + ) + process_task_id = process_task["task_id"] + # Check status of task + progress_msg = f"Check processing task status..." + self.report_progress(progress_msg) + task_processed = False + while not task_processed: + task_status = self.downloader.get_task_status(process_task_id) + if task_status in success_statuses: + task_processed = True + elif task_status in in_progress_statuses: + continue + else: + error_msg = f"Task {process_task_id} failed, status was: {task_status}" + raise LizardFloodRiskAnalysisError(error_msg) + time.sleep(self.TASK_CHECK_SLEEP_TIME) + + @pyqtSlot() + def run(self): + """Run flood risk analysis for buildings.""" + try: + self.report_progress(increase_current_step=False) + self.analyze_buildings_flood_risk() + self.report_finished("Scenario flood risk analysis finished.") + except LizardFloodRiskAnalysisError as e: + self.report_failure(str(e)) + except Exception as e: + error_msg = f"Buildings flood risk analysis failed due to the following error: {e}" + self.report_failure(error_msg) + + def report_progress(self, progress_message=None, increase_current_step=True): + """Report worker progress.""" + current_progress = int(self.current_step * self.percentage_per_step) + if increase_current_step: + self.current_step += 1 + self.signals.analysis_progress.emit( + self.scenario_instance, progress_message, current_progress, self.total_progress + ) + + def report_failure(self, error_message): + """Report worker failure message.""" + self.signals.analysis_failed.emit(self.scenario_instance, error_message) + + def report_finished(self, message): + """Report worker finished message.""" + self.signals.analysis_finished.emit(self.scenario_instance, message) From bf39535377fec230b1e0cb401f6456d6833d52eb Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Tue, 8 Oct 2024 19:03:11 +0200 Subject: [PATCH 05/10] Work on buildings flood risk analysis. --- .../widgets/lizard_archive_browser.py | 69 ++++++++- .../widgets/ui/buildings_flood_risk.ui | 146 ++++++++++++++++++ lizard_qgis_plugin/workers.py | 2 +- 3 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 18c9a19..25fa8e8 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -36,13 +36,56 @@ try_to_write, unify_spatial_boundaries, ) -from lizard_qgis_plugin.workers import RasterDownloader, ScenarioItemsDownloader +from lizard_qgis_plugin.workers import BuildingsFloodRiskAnalyzer, RasterDownloader, ScenarioItemsDownloader base_dir = os.path.dirname(__file__) lizard_uicls, lizard_basecls = uic.loadUiType(os.path.join(base_dir, "ui", "lizard.ui")) download_settings_uicls, download_settings_basecls = uic.loadUiType( os.path.join(base_dir, "ui", "raster_download_settings.ui") ) +buildings_flood_risk_uicls, buildings_flood_risk_basecls = uic.loadUiType( + os.path.join(base_dir, "ui", "buildings_flood_risk.ui") +) + + +class BuildingsFloodRiskDialog(buildings_flood_risk_uicls, buildings_flood_risk_basecls): + def __init__(self, lizard_browser, parent=None): + super().__init__(parent) + self.setupUi(self) + self.lizard_browser = lizard_browser + self.buildings_layer_cbo.setFilters(QgsMapLayerProxyModel.PolygonLayer) + self.floor_level_field_cbo.setFilters(QgsFieldProxyModel.Numeric) + self.method_cbo.currentIndexChanged.connect(self.on_method_changed) + self.buildings_layer_cbo.layerChanged.connect(self.on_buildings_polygon_changed) + self.floor_level_field_cbo.setLayer(self.buildings_layer_cbo.currentLayer()) + self.accept_pb.clicked.connect(self.accept) + self.cancel_pb.clicked.connect(self.reject) + self.populate_flood_risk_buildings_calculation_methods() + + def on_method_changed(self): + """Enable/disable buildings floor level column widgets.""" + if self.method_cbo.currentText() == "Advanced": + self.floor_level_label.setEnabled(True) + self.floor_level_field_cbo.setEnabled(True) + else: + self.floor_level_label.setDisabled(True) + self.floor_level_field_cbo.setDisabled(True) + + def on_buildings_polygon_changed(self, layer): + """Refresh field list on buildings polygon change.""" + self.floor_level_field_cbo.setLayer(layer) + if layer is None: + self.accept_pb.setDisabled(True) + else: + self.accept_pb.setEnabled(True) + + def populate_flood_risk_buildings_calculation_methods(self): + """Populate flood risk buildings calculation methods.""" + for method_name in self.lizard_browser.current_scenario_flood_risk_methods: + self.method_cbo.addItem(method_name, method_name.lower()) + self.output_format_cbo.addItem("GeoPackage", "gpkg") + self.output_format_cbo.addItem("GeoJSON", "geojson") + self.on_buildings_polygon_changed(self.buildings_layer_cbo.currentLayer()) class RasterDownloadSettings(download_settings_uicls, download_settings_basecls): @@ -123,6 +166,7 @@ def __init__(self, plugin, parent=None): self.feedback_lv.setModel(self.feedback_model) self.current_scenario_instances = {} self.current_scenario_results = {} + self.current_scenario_flood_risk_methods = [] self.current_raster_instances = {} self.pb_prev_page.clicked.connect(self.previous_scenarios) self.pb_next_page.clicked.connect(self.next_scenarios) @@ -130,6 +174,7 @@ def __init__(self, plugin, parent=None): self.pb_add_wms.clicked.connect(self.load_scenario_as_wms_layers) self.pb_show_files.clicked.connect(self.fetch_results) self.pb_download.clicked.connect(self.download_results) + self.pb_flood_risk_buildings.clicked.connect(self.analyse_flood_risk_buildings) self.toggle_selection_ckb.stateChanged.connect(self.toggle_results) self.scenario_search_le.returnPressed.connect(self.search_for_scenarios) self.scenario_tv.selectionModel().selectionChanged.connect(self.toggle_scenario_selected) @@ -163,10 +208,12 @@ def toggle_scenario_selected(self): """Toggle action widgets if any scenario is selected.""" self.scenario_results_model.clear() self.current_scenario_results.clear() + self.current_scenario_flood_risk_methods.clear() self.pb_download.setDisabled(True) self.grp_raster_settings.setDisabled(True) self.toggle_selection_ckb.setChecked(False) self.toggle_selection_ckb.setDisabled(True) + self.pb_flood_risk_buildings.setDisabled(True) selection_model = self.scenario_tv.selectionModel() if selection_model.hasSelection(): self.pb_add_wms.setEnabled(True) @@ -269,6 +316,7 @@ def fetch_scenarios(self): self.scenario_model.clear() self.current_scenario_results.clear() self.scenario_results_model.clear() + self.current_scenario_flood_risk_methods.clear() offset = (self.page_sbox.value() - 1) * self.TABLE_LIMIT header = ["Scenario name", "Model name", "Organisation", "User", "Created", "UUID"] self.scenario_model.setHorizontalHeaderLabels(header) @@ -305,6 +353,7 @@ def fetch_results(self): scenario_results = self.plugin.downloader.get_scenario_instance_results(scenario_uuid) self.current_scenario_results.clear() self.scenario_results_model.clear() + self.current_scenario_flood_risk_methods.clear() header, checkboxes_width = ["Item", "File name"], [] self.scenario_results_model.setHorizontalHeaderLabels(header) for row_number, result in enumerate(scenario_results, start=0): @@ -313,6 +362,11 @@ def fetch_results(self): result_name = result["name"] result_attachment_url = result["attachment_url"] result_raster = result["raster"] + result_code = result["code"] + if result_code == "depth-max-dtri": + self.current_scenario_flood_risk_methods.append("DGBC") + elif result_code == "s1-max-dtri": + self.current_scenario_flood_risk_methods.append("Advanced") if result_raster: raster_instance = get_url_raster_instance(self.plugin.downloader.get_api_key(), result_raster) if raster_instance["temporal"]: @@ -338,6 +392,8 @@ def fetch_results(self): self.scenario_results_tv.resizeColumnToContents(i) if checkboxes_width: self.scenario_results_tv.setColumnWidth(0, max(checkboxes_width)) + if self.current_scenario_flood_risk_methods: + self.pb_flood_risk_buildings.setEnabled(True) self.pb_download.setEnabled(True) self.grp_raster_settings.setEnabled(True) self.toggle_selection_ckb.setEnabled(True) @@ -380,13 +436,16 @@ def load_scenario_as_wms_layers(self): self.log_feedback(f"WMS layers for scenario '{scenario_name}' added to the project.") def analyse_flood_risk_buildings(self): + """Setup buildings flood risk analysis.""" index = self.scenario_tv.currentIndex() if not index.isValid(): return - current_row = index.row() - scenario_uuid_item = self.scenario_model.item(current_row, self.SCENARIO_UUID_COLUMN_IDX) - scenario_uuid = scenario_uuid_item.text() - # TODO: Continue here + buildings_flood_risk_dlg = BuildingsFloodRiskDialog(self) + res = buildings_flood_risk_dlg.exec_() + if res != QDialog.Accepted: + self.raise_() + return + # TODO: Export building features and start the worker. def download_results(self): """Download selected (checked) result files.""" diff --git a/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui b/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui new file mode 100644 index 0000000..57675c7 --- /dev/null +++ b/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui @@ -0,0 +1,146 @@ + + + Dialog + + + + 0 + 0 + 794 + 432 + + + + + Segoe UI + 10 + + + + Analyse flood risk for buildings + + + + + + false + + + + Segoe UI + 10 + + + + Floor level column: + + + + + + + + Segoe UI + 10 + + + + Buildings polygon layer: + + + + + + + + + + + + + + Segoe UI + 12 + + + + Cancel + + + + + + + Analysis method: + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + true + + + + Segoe UI + 12 + 75 + true + + + + Accept + + + + + + + false + + + + + + + Output format: + + + + + + + + + + + QgsFieldComboBox + QComboBox +
qgsfieldcombobox.h
+
+ + QgsMapLayerComboBox + QComboBox +
qgsmaplayercombobox.h
+
+
+ + accept_pb + cancel_pb + + + +
diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index edc2981..a63d879 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -404,7 +404,7 @@ def analyze_buildings_flood_risk(self): progress_msg = f"Create a \"vulnerable buildings\" result object for the scenario '{self.scenario_name}'..." self.report_progress(progress_msg) vulnerable_buildings_result = create_vulnerable_buildings_result(lizard_url, api_key, self.scenario_instance) - result_id = vulnerable_buildings_result["result_id"] + result_id = vulnerable_buildings_result["id"] progress_msg = f'Spawn processing of the "vulnerable buildings" result task...' self.report_progress(progress_msg) process_task = create_buildings_flood_risk_task( From 6ea7dee3e16923ee061069ccc242f1d1d2ce26d7 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Wed, 9 Oct 2024 18:36:58 +0200 Subject: [PATCH 06/10] Work on buildings flood risk analysis. --- lizard_qgis_plugin/__init__.py | 4 +- lizard_qgis_plugin/utils.py | 28 +++- .../widgets/lizard_archive_browser.py | 49 +++++- .../widgets/ui/buildings_flood_risk.ui | 157 +++++++++++------- lizard_qgis_plugin/workers.py | 1 + 5 files changed, 168 insertions(+), 71 deletions(-) diff --git a/lizard_qgis_plugin/__init__.py b/lizard_qgis_plugin/__init__.py index c604b61..d101e02 100644 --- a/lizard_qgis_plugin/__init__.py +++ b/lizard_qgis_plugin/__init__.py @@ -35,8 +35,8 @@ def __init__(self, iface): self.iface = iface self.plugin_dir = os.path.dirname(__file__) self.downloader = downloader - self.lizard_downloader_pool = QThreadPool() - self.lizard_downloader_pool.setMaxThreadCount(self.MAX_DOWNLOAD_THREAD_COUNT) + self.lizard_tasks_pool = QThreadPool() + self.lizard_tasks_pool.setMaxThreadCount(self.MAX_DOWNLOAD_THREAD_COUNT) self.lizard_browser = None self.actions = [] self.menu = self.PLUGIN_NAME diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index 24ebad0..3f614a4 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -352,8 +352,9 @@ def create_buildings_result(lizard_url, api_key, scenario_instance): """Create Lizard buildings result.""" scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/" + headers = {"content-type": "application/json", "Accept-Charset": "UTF-8"} payload = {"name": "buildings", "code": "buildings", "family": "Raw"} - r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r = requests.post(url=url, auth=("__key__", api_key), data=payload, headers=headers) r.raise_for_status() buildings_result = r.json() return buildings_result @@ -364,7 +365,7 @@ def create_vulnerable_buildings_result(lizard_url, api_key, scenario_instance): scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/" payload = {"name": "vulnerable_buildings", "code": "vulnerable_buildings", "family": "Vulnerable_Buildings"} - r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r = requests.post(url=url, auth=("__key__", api_key), data=payload) r.raise_for_status() vulnerable_buildings_result = r.json() return vulnerable_buildings_result @@ -435,6 +436,29 @@ def wkt_polygon_layer(polygon_wkt, polygon_layer_name="clip_layer", epsg="EPSG:4 return memory_polygon_layer +def spawn_memory_buildings_layer(building_features, floor_level_field_name=None, epsg="EPSG:4326"): + """Spawn building polygons layer out of the derived features.""" + geometry_type = "Polygon" + uri = f"{geometry_type}?crs={epsg}" + memory_buildings_layer = QgsVectorLayer(uri, "buildings", "memory") + memory_buildings_layer_dt = memory_buildings_layer.dataProvider() + memory_buildings_layer_dt.addAttributes([QgsField("floor_level", QVariant.Double)]) + memory_buildings_layer.updateFields() + memory_buildings_fields = memory_buildings_layer.fields() + new_building_features = [] + for feature in building_features: + new_building_feat = QgsFeature(memory_buildings_fields) + new_building_geom = QgsGeometry(feature.geometry()) + new_building_feat.setGeometry(new_building_geom) + if floor_level_field_name: + new_building_feat["floor_level"] = feature[floor_level_field_name] + new_building_features.append(new_building_feat) + memory_buildings_layer.startEditing() + memory_buildings_layer.addFeatures(new_building_features) + memory_buildings_layer.commitChanges() + return memory_buildings_layer + + def layer_to_gpkg(layer, gpkg_filename, overwrite=False, driver_name="GPKG"): """Function which saves memory layer into GeoPackage file.""" transform_context = QgsProject.instance().transformContext() diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 25fa8e8..7edb133 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -4,6 +4,7 @@ from copy import deepcopy from math import ceil from operator import itemgetter +from tempfile import gettempdir from qgis.core import ( Qgis, @@ -32,7 +33,9 @@ find_rasters, get_capabilities_layer_uris, get_url_raster_instance, + layer_to_gpkg, reproject_geometry, + spawn_memory_buildings_layer, try_to_write, unify_spatial_boundaries, ) @@ -54,7 +57,7 @@ def __init__(self, lizard_browser, parent=None): self.setupUi(self) self.lizard_browser = lizard_browser self.buildings_layer_cbo.setFilters(QgsMapLayerProxyModel.PolygonLayer) - self.floor_level_field_cbo.setFilters(QgsFieldProxyModel.Numeric) + self.floor_level_field_cbo.setFilters(QgsFieldProxyModel.Double) self.method_cbo.currentIndexChanged.connect(self.on_method_changed) self.buildings_layer_cbo.layerChanged.connect(self.on_buildings_polygon_changed) self.floor_level_field_cbo.setLayer(self.buildings_layer_cbo.currentLayer()) @@ -74,6 +77,7 @@ def on_method_changed(self): def on_buildings_polygon_changed(self, layer): """Refresh field list on buildings polygon change.""" self.floor_level_field_cbo.setLayer(layer) + self.floor_level_field_cbo.setCurrentText("floor_level") if layer is None: self.accept_pb.setDisabled(True) else: @@ -85,7 +89,10 @@ def populate_flood_risk_buildings_calculation_methods(self): self.method_cbo.addItem(method_name, method_name.lower()) self.output_format_cbo.addItem("GeoPackage", "gpkg") self.output_format_cbo.addItem("GeoJSON", "geojson") - self.on_buildings_polygon_changed(self.buildings_layer_cbo.currentLayer()) + current_layer = self.buildings_layer_cbo.currentLayer() + self.on_buildings_polygon_changed(current_layer) + if current_layer is not None and current_layer.selectedFeatureCount() > 0: + self.selected_buildings_cb.setChecked(True) class RasterDownloadSettings(download_settings_uicls, download_settings_basecls): @@ -440,12 +447,44 @@ def analyse_flood_risk_buildings(self): index = self.scenario_tv.currentIndex() if not index.isValid(): return + current_row = index.row() + scenario_uuid_item = self.scenario_model.item(current_row, self.SCENARIO_UUID_COLUMN_IDX) + scenario_uuid = scenario_uuid_item.text() + scenario_instance = self.current_scenario_instances[scenario_uuid] + scenario_name = scenario_instance["name"] buildings_flood_risk_dlg = BuildingsFloodRiskDialog(self) res = buildings_flood_risk_dlg.exec_() if res != QDialog.Accepted: self.raise_() return - # TODO: Export building features and start the worker. + analysis_method = buildings_flood_risk_dlg.method_cbo.currentData() + buildings_layer = buildings_flood_risk_dlg.buildings_layer_cbo.currentLayer() + selected_buildings_only = buildings_flood_risk_dlg.selected_buildings_cb.isChecked() + floor_level_field = ( + buildings_flood_risk_dlg.floor_level_field_cbo.currentField() + if buildings_flood_risk_dlg.floor_level_field_cbo.isEnabled() + else None + ) + output_format = buildings_flood_risk_dlg.output_format_cbo.currentData() + if selected_buildings_only and buildings_layer.selectedFeatureCount() == 0: + warn_message = "No building features selected - please select features and try again." + self.log_feedback(warn_message, Qgis.Warning) + return + epsg = buildings_layer.crs().authid() + src_features = ( + list(buildings_layer.selectedFeatures()) if selected_buildings_only else list(buildings_layer.getFeatures()) + ) + buildings_mem_layer = spawn_memory_buildings_layer(src_features, floor_level_field, epsg) + buildings_gpkg_path = os.path.join(gettempdir(), "buildings.gpkg") + layer_to_gpkg(buildings_mem_layer, buildings_gpkg_path, overwrite=True) + buildings_flood_risk_analyzer = BuildingsFloodRiskAnalyzer( + self.plugin.downloader, scenario_instance, buildings_gpkg_path, analysis_method, output_format + ) + buildings_flood_risk_analyzer.signals.analysis_progress.connect(self.on_flood_risk_analysis_progress) + buildings_flood_risk_analyzer.signals.analysis_finished.connect(self.on_flood_risk_analysis_finished) + buildings_flood_risk_analyzer.signals.analysis_failed.connect(self.on_flood_risk_analysis_failed) + self.plugin.lizard_tasks_pool.start(buildings_flood_risk_analyzer) + self.log_feedback(f"Scenario '{scenario_name}' buildings flood risk analysis task added to the queue.") def download_results(self): """Download selected (checked) result files.""" @@ -499,7 +538,7 @@ def download_results(self): scenario_items_downloader.signals.download_progress.connect(self.on_download_progress) scenario_items_downloader.signals.download_finished.connect(self.on_download_finished) scenario_items_downloader.signals.download_failed.connect(self.on_download_failed) - self.plugin.lizard_downloader_pool.start(scenario_items_downloader) + self.plugin.lizard_tasks_pool.start(scenario_items_downloader) self.log_feedback(f"Scenario '{scenario_name}' results download task added to the queue.") def on_download_progress(self, downloaded_item_instance, progress_message, current_progress, total_progress): @@ -766,5 +805,5 @@ def download_raster_file(self): raster_downloader.signals.download_progress.connect(self.on_download_progress) raster_downloader.signals.download_finished.connect(self.on_download_finished) raster_downloader.signals.download_failed.connect(self.on_download_failed) - self.plugin.lizard_downloader_pool.start(raster_downloader) + self.plugin.lizard_tasks_pool.start(raster_downloader) self.log_feedback(f"Raster '{raster_name}' download task added to the queue.") diff --git a/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui b/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui index 57675c7..326572a 100644 --- a/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui +++ b/lizard_qgis_plugin/widgets/ui/buildings_flood_risk.ui @@ -6,8 +6,8 @@ 0 0 - 794 - 432 + 650 + 400
@@ -20,19 +20,10 @@ Analyse flood risk for buildings - - - - false - - - - Segoe UI - 10 - - + + - Floor level column: + Analysis method: @@ -49,33 +40,13 @@ - - - - - - - - Segoe UI - 12 - - - - Cancel - - - - - - - Analysis method: - - + + - + Qt::Vertical @@ -88,40 +59,106 @@ - - + + - true - - - - Segoe UI - 12 - 75 - true - + false + + + + - Accept + Output format: - - - - false + + + + + + + 0 - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::LeftToRight + + + Selected features only + + + +
- + + + false + + + + Segoe UI + 10 + + - Output format: + Floor level column: - - + + + + 0 + + + + + + Segoe UI + 12 + + + + Cancel + + + + + + + true + + + + Segoe UI + 12 + 75 + true + + + + Accept + + + + @@ -137,10 +174,6 @@
qgsmaplayercombobox.h
- - accept_pb - cancel_pb - diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index a63d879..7d26ee3 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -379,6 +379,7 @@ def __init__( super().__init__() self.downloader = downloader self.scenario_instance = scenario_instance + self.scenario_name = self.scenario_instance["name"] self.buildings_gpkg = buildings_gpkg self.calculation_method = calculation_method self.output_format = output_format From bd07e7af893fc8f4f03f7bfeca6c5f65eeda4b29 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Thu, 10 Oct 2024 15:09:08 +0200 Subject: [PATCH 07/10] Work on buildings flood risk analysis. --- lizard_qgis_plugin/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index 3f614a4..b3381c6 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -352,9 +352,8 @@ def create_buildings_result(lizard_url, api_key, scenario_instance): """Create Lizard buildings result.""" scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/" - headers = {"content-type": "application/json", "Accept-Charset": "UTF-8"} payload = {"name": "buildings", "code": "buildings", "family": "Raw"} - r = requests.post(url=url, auth=("__key__", api_key), data=payload, headers=headers) + r = requests.post(url=url, auth=("__key__", api_key), data=payload) r.raise_for_status() buildings_result = r.json() return buildings_result @@ -378,7 +377,7 @@ def create_buildings_flood_risk_task( scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/{result_id}/process/" payload = {"method": calculation_method, "output_format": output_format} - r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r = requests.post(url=url, auth=("__key__", api_key), data=payload) r.raise_for_status() process_task = r.json() return process_task From 53e9b7a9d4507b7752ab7e80a7edbb7d3633d027 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Thu, 10 Oct 2024 18:17:10 +0200 Subject: [PATCH 08/10] Work on buildings flood risk analysis. --- lizard_qgis_plugin/utils.py | 20 +++++++++++++++++--- lizard_qgis_plugin/workers.py | 9 +++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index b3381c6..8b45f3d 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -348,12 +348,26 @@ def upload_local_file(upload_url, local_filepath): return response +def clean_up_buildings_result(lizard_url, api_key, scenario_instance, limit=100): + """Remove buildings results from scenario instance.""" + building_codes = {"buildings", "vulnerable_buildings"} + scenario_id = scenario_instance["uuid"] + url = f"{lizard_url}scenarios/{scenario_id}/results/" + results_response = requests.get(url=url, auth=("__key__", api_key), params={"limit": limit}) + results_response.raise_for_status() + existing_results = [res for res in results_response.json()["results"] if res["code"] in building_codes] + for res in existing_results: + res_id = res["id"] + delete_url = f"{url}{res_id}/" + requests.delete(url=delete_url, auth=("__key__", api_key)) + + def create_buildings_result(lizard_url, api_key, scenario_instance): """Create Lizard buildings result.""" scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/" payload = {"name": "buildings", "code": "buildings", "family": "Raw"} - r = requests.post(url=url, auth=("__key__", api_key), data=payload) + r = requests.post(url=url, auth=("__key__", api_key), json=payload) r.raise_for_status() buildings_result = r.json() return buildings_result @@ -364,7 +378,7 @@ def create_vulnerable_buildings_result(lizard_url, api_key, scenario_instance): scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/" payload = {"name": "vulnerable_buildings", "code": "vulnerable_buildings", "family": "Vulnerable_Buildings"} - r = requests.post(url=url, auth=("__key__", api_key), data=payload) + r = requests.post(url=url, auth=("__key__", api_key), json=payload) r.raise_for_status() vulnerable_buildings_result = r.json() return vulnerable_buildings_result @@ -377,7 +391,7 @@ def create_buildings_flood_risk_task( scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/{result_id}/process/" payload = {"method": calculation_method, "output_format": output_format} - r = requests.post(url=url, auth=("__key__", api_key), data=payload) + r = requests.get(url=url, auth=("__key__", api_key), params=payload) r.raise_for_status() process_task = r.json() return process_task diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index 7d26ee3..e6d5388 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -13,6 +13,7 @@ from lizard_qgis_plugin.utils import ( build_vrt, + clean_up_buildings_result, clip_raster, create_buildings_flood_risk_task, create_buildings_result, @@ -385,7 +386,7 @@ def __init__( self.output_format = output_format self.total_progress = 100 self.current_step = 0 - self.number_of_steps = 4 + self.number_of_steps = 5 self.percentage_per_step = self.total_progress / self.number_of_steps self.signals = LizardFloodRiskAnalysisSignals() @@ -394,6 +395,10 @@ def analyze_buildings_flood_risk(self): in_progress_statuses = {"PENDING", "UNKNOWN", "STARTED", "RETRY"} lizard_url = self.downloader.LIZARD_URL api_key = self.downloader.get_api_key() + # Remove existing buildings results objects from the scenario + progress_msg = f"Remove existing \"buildings\" results objects from the scenario '{self.scenario_name}'..." + self.report_progress(progress_msg) + clean_up_buildings_result(lizard_url, api_key, self.scenario_instance) # Create a "buildings" result object for the scenario progress_msg = f"Create a \"buildings\" result object for the scenario '{self.scenario_name}'..." self.report_progress(progress_msg) @@ -401,7 +406,7 @@ def analyze_buildings_flood_risk(self): progress_msg = f"Upload buildings for the scenario '{self.scenario_name}'..." self.report_progress(progress_msg) buildings_result_upload_url = buildings_result["upload_url"] - upload_local_file(self.buildings_gpkg, buildings_result_upload_url) + upload_local_file(buildings_result_upload_url, self.buildings_gpkg) progress_msg = f"Create a \"vulnerable buildings\" result object for the scenario '{self.scenario_name}'..." self.report_progress(progress_msg) vulnerable_buildings_result = create_vulnerable_buildings_result(lizard_url, api_key, self.scenario_instance) From 8a46a4786fd51caed467a5d892494e388809cd4c Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Thu, 10 Oct 2024 18:21:15 +0200 Subject: [PATCH 09/10] Work on buildings flood risk analysis. --- lizard_qgis_plugin/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index 8b45f3d..09edc15 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -391,7 +391,7 @@ def create_buildings_flood_risk_task( scenario_id = scenario_instance["uuid"] url = f"{lizard_url}scenarios/{scenario_id}/results/{result_id}/process/" payload = {"method": calculation_method, "output_format": output_format} - r = requests.get(url=url, auth=("__key__", api_key), params=payload) + r = requests.post(url=url, auth=("__key__", api_key), json=payload) r.raise_for_status() process_task = r.json() return process_task From a509fcd4eded73a6b9586fef0bb45217f52f5cb8 Mon Sep 17 00:00:00 2001 From: "lukasz.debek" Date: Wed, 16 Oct 2024 15:42:44 +0200 Subject: [PATCH 10/10] Implemented ability to run buildings flood risk analysis (#36). --- HISTORY.rst | 1 + lizard_qgis_plugin/utils.py | 13 +++++++++++++ .../widgets/lizard_archive_browser.py | 4 +++- lizard_qgis_plugin/workers.py | 8 ++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d061dad..6d35d64 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ History ------------------- - Fixes/enhancements: #35, #37 +- Implemented ability to run buildings flood risk analysis (#36). 0.3.5 (2024-9-12) diff --git a/lizard_qgis_plugin/utils.py b/lizard_qgis_plugin/utils.py index 09edc15..6b535ab 100644 --- a/lizard_qgis_plugin/utils.py +++ b/lizard_qgis_plugin/utils.py @@ -124,6 +124,19 @@ def get_capabilities_layer_uris(wms_url): return wms_uris +def get_scenario_instance_results(lizard_url, scenario_instance, subendpoint=None, results_limit=100): + """Get the scenario instance results, either from basic endpoint, or specific subendpoint.""" + scenario_uuid = scenario_instance["uuid"] + if subendpoint: + url = f"{lizard_url}scenarios/{scenario_uuid}/results/{subendpoint}" + else: + url = f"{lizard_url}scenarios/{scenario_uuid}/results" + r = requests.get(url=url, auth=("__key__", get_api_key_auth_manager()), params={"limit": results_limit}) + r.raise_for_status() + available_results = r.json()["results"] + return available_results + + def get_available_rasters_list(lizard_url): """List all available rasters.""" url = f"{lizard_url}rasters/" diff --git a/lizard_qgis_plugin/widgets/lizard_archive_browser.py b/lizard_qgis_plugin/widgets/lizard_archive_browser.py index 7edb133..b0b65ee 100644 --- a/lizard_qgis_plugin/widgets/lizard_archive_browser.py +++ b/lizard_qgis_plugin/widgets/lizard_archive_browser.py @@ -32,6 +32,7 @@ create_tree_group, find_rasters, get_capabilities_layer_uris, + get_scenario_instance_results, get_url_raster_instance, layer_to_gpkg, reproject_geometry, @@ -357,7 +358,7 @@ def fetch_results(self): scenario_uuid_item = self.scenario_model.item(current_row, self.SCENARIO_UUID_COLUMN_IDX) scenario_uuid = scenario_uuid_item.text() scenario_instance = self.current_scenario_instances[scenario_uuid] - scenario_results = self.plugin.downloader.get_scenario_instance_results(scenario_uuid) + scenario_results = get_scenario_instance_results(self.plugin.downloader.LIZARD_URL, scenario_instance) self.current_scenario_results.clear() self.scenario_results_model.clear() self.current_scenario_flood_risk_methods.clear() @@ -580,6 +581,7 @@ def on_flood_risk_analysis_finished(self, scenario_instance, message): self.plugin.communication.clear_message_bar() self.plugin.communication.bar_info(message) self.log_feedback(message) + self.fetch_results() def on_flood_risk_analysis_failed(self, scenario_instance, error_message): """Feedback on flood risk analysis failed signal.""" diff --git a/lizard_qgis_plugin/workers.py b/lizard_qgis_plugin/workers.py index e6d5388..10ccbc3 100644 --- a/lizard_qgis_plugin/workers.py +++ b/lizard_qgis_plugin/workers.py @@ -386,7 +386,7 @@ def __init__( self.output_format = output_format self.total_progress = 100 self.current_step = 0 - self.number_of_steps = 5 + self.number_of_steps = 6 self.percentage_per_step = self.total_progress / self.number_of_steps self.signals = LizardFloodRiskAnalysisSignals() @@ -411,6 +411,7 @@ def analyze_buildings_flood_risk(self): self.report_progress(progress_msg) vulnerable_buildings_result = create_vulnerable_buildings_result(lizard_url, api_key, self.scenario_instance) result_id = vulnerable_buildings_result["id"] + time.sleep(self.TASK_CHECK_SLEEP_TIME) progress_msg = f'Spawn processing of the "vulnerable buildings" result task...' self.report_progress(progress_msg) process_task = create_buildings_flood_risk_task( @@ -442,7 +443,10 @@ def run(self): except LizardFloodRiskAnalysisError as e: self.report_failure(str(e)) except Exception as e: - error_msg = f"Buildings flood risk analysis failed due to the following error: {e}" + try: + error_msg = f"Buildings flood risk analysis failed due to the following error: {e.response.text}" + except AttributeError: + error_msg = f"Buildings flood risk analysis failed due to the following error: {e}" self.report_failure(error_msg) def report_progress(self, progress_message=None, increase_current_step=True):