From 7b0c84005b4bde8414583314ec3f414fc8af1743 Mon Sep 17 00:00:00 2001 From: Danang Massandy Date: Thu, 12 Sep 2024 10:52:52 +0000 Subject: [PATCH 1/5] use core package for ScenarioAnalysisTask --- deployment/docker/Dockerfile | 4 +- deployment/docker/requirements.txt | 5 +- django_project/core/settings/project.py | 1 - django_project/cplus/__init__.py | 0 django_project/cplus/admin.py | 1 - django_project/cplus/apps.py | 6 - .../cplus/data/default/activities.json | 334 -- .../cplus/data/default/ncs_pathways.json | 125 - .../default/priority_weighting_layers.json | 60 - .../cplus/data/layers/null_raster.tif | Bin 442282 -> 0 bytes django_project/cplus/data/reports/main.qpt | 2878 ----------------- django_project/cplus/definitions/__init__.py | 2 - django_project/cplus/definitions/constants.py | 62 - django_project/cplus/definitions/defaults.py | 232 -- django_project/cplus/migrations/__init__.py | 0 django_project/cplus/models/__init__.py | 0 django_project/cplus/models/base.py | 586 ---- django_project/cplus/models/financial.py | 167 - django_project/cplus/models/helpers.py | 594 ---- django_project/cplus/models/report.py | 64 - django_project/cplus/tasks/__init__.py | 0 django_project/cplus/tasks/analysis.py | 2151 ------------ django_project/cplus/tests.py | 1 - django_project/cplus/utils/__init__.py | 0 django_project/cplus/utils/conf.py | 1333 -------- django_project/cplus/utils/helper.py | 619 ---- django_project/cplus/views.py | 1 - django_project/cplus_api/tasks/runner.py | 4 +- .../cplus_api/utils/worker_analysis.py | 103 +- 29 files changed, 74 insertions(+), 9259 deletions(-) delete mode 100644 django_project/cplus/__init__.py delete mode 100644 django_project/cplus/admin.py delete mode 100644 django_project/cplus/apps.py delete mode 100644 django_project/cplus/data/default/activities.json delete mode 100644 django_project/cplus/data/default/ncs_pathways.json delete mode 100644 django_project/cplus/data/default/priority_weighting_layers.json delete mode 100644 django_project/cplus/data/layers/null_raster.tif delete mode 100644 django_project/cplus/data/reports/main.qpt delete mode 100644 django_project/cplus/definitions/__init__.py delete mode 100644 django_project/cplus/definitions/constants.py delete mode 100644 django_project/cplus/definitions/defaults.py delete mode 100644 django_project/cplus/migrations/__init__.py delete mode 100644 django_project/cplus/models/__init__.py delete mode 100644 django_project/cplus/models/base.py delete mode 100644 django_project/cplus/models/financial.py delete mode 100644 django_project/cplus/models/helpers.py delete mode 100644 django_project/cplus/models/report.py delete mode 100644 django_project/cplus/tasks/__init__.py delete mode 100644 django_project/cplus/tasks/analysis.py delete mode 100644 django_project/cplus/tests.py delete mode 100644 django_project/cplus/utils/__init__.py delete mode 100644 django_project/cplus/utils/conf.py delete mode 100644 django_project/cplus/utils/helper.py delete mode 100644 django_project/cplus/views.py diff --git a/deployment/docker/Dockerfile b/deployment/docker/Dockerfile index 1afc822..2cc428a 100644 --- a/deployment/docker/Dockerfile +++ b/deployment/docker/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update -y && \ python3-dev python3-gdal python3-psycopg2 python3-ldap \ python3-pip python3-pil python3-lxml python3-pylibmc \ uwsgi uwsgi-plugin-python3 wget \ - gnupg software-properties-common + gnupg software-properties-common git # qgis python3-qgis qgis-plugin-grass RUN mkdir -m755 -p /etc/apt/keyrings @@ -62,7 +62,7 @@ RUN groupadd --gid $USER_GID $USERNAME \ # ******************************************************** # * Anything else you want to do like clean up goes here * # ******************************************************** -RUN apt-get update && apt-get install -y git gnupg2 openssh-client +RUN apt-get update && apt-get install -y gnupg2 openssh-client # [Optional] Set the default user. Omit if you want to keep the default as root. USER $USERNAME diff --git a/deployment/docker/requirements.txt b/deployment/docker/requirements.txt index 6b73c4d..67a4948 100644 --- a/deployment/docker/requirements.txt +++ b/deployment/docker/requirements.txt @@ -60,4 +60,7 @@ flower==1.2.0 django-revproxy @ https://github.com/jazzband/django-revproxy/archive/refs/tags/0.11.0.zip # For getting raster metadata -rasterio==1.3.10 \ No newline at end of file +rasterio==1.3.10 + +# cplus core +git+https://github.com/kartoza/cplus-core.git@feat-refactor-conf diff --git a/django_project/core/settings/project.py b/django_project/core/settings/project.py index c4e99cf..74ddd21 100644 --- a/django_project/core/settings/project.py +++ b/django_project/core/settings/project.py @@ -34,7 +34,6 @@ # Extra installed apps INSTALLED_APPS = INSTALLED_APPS + ( 'core', - 'cplus', 'cplus_api' ) diff --git a/django_project/cplus/__init__.py b/django_project/cplus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_project/cplus/admin.py b/django_project/cplus/admin.py deleted file mode 100644 index 846f6b4..0000000 --- a/django_project/cplus/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/django_project/cplus/apps.py b/django_project/cplus/apps.py deleted file mode 100644 index 48f4501..0000000 --- a/django_project/cplus/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CplusConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'cplus' diff --git a/django_project/cplus/data/default/activities.json b/django_project/cplus/data/default/activities.json deleted file mode 100644 index 439dd8d..0000000 --- a/django_project/cplus/data/default/activities.json +++ /dev/null @@ -1,334 +0,0 @@ -{ - "activities": [ - { - "uuid": "a0b8fd2d-1259-4141-9ad6-d4369cf0dfd4", - "name": "Agroforestry", - "description": " Agroforestry is an integrated land use system that combines the cultivation of trees with agricultural crops and/or livestock. It promotes sustainable land management, biodiversity conservation, soil health improvement, and diversified income sources for farmers.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#d80007", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "Reds"} - } - }, - { - "uuid": "1c8db48b-717b-451b-a644-3af1bee984ea", - "name": "Alien Plant Removal", - "description": "This model involves the removal of invasive alien plant species that negatively impact native ecosystems. By eradicating these plants, natural habitats can be restored, allowing native flora and fauna to thrive.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "3c155210-ccd8-404b-bbe8-b1433d6158a2", - "9f6c8b8f-0648-44ca-b943-58fab043f559", - "9291a5d9-d1cd-44c2-8fc3-2b3b20f80572" - ], - "style": { - "scenario_layer": { - "color": "#6f6f6f", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "Greys"} - } - }, - { - "uuid": "de9597b2-f082-4299-9620-1da3bad8ab62", - "name": "Applied Nucleation", - "description": " Applied nucleation is a technique that jump-starts the restoration process by creating focal points of vegetation growth within degraded areas. These 'nuclei' serve as centers for biodiversity recovery, attracting seeds, dispersers, and other ecological processes, ultimately leading to the regeneration of the surrounding landscape.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#81c4ff", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "PuBu"} - } - }, - { - "uuid": "40f04ea6-1f91-4695-830a-7d46f821f5db", - "name": "Assisted Natural Regeneration", - "description": " This model focuses on facilitating the natural regeneration of forests and degraded lands by removing barriers (such as alien plants or hard crusted soils) and providing support for native plant species to regrow. It involves activities such as removing competing vegetation, protecting young seedlings, and restoring ecosystem functions.", - "pwls_ids": [ - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "85cd441e-fa3d-46e4-add9-973ba58f8bd4", - "5e41f4fa-3d7f-41aa-bee7-b9e9d08b56db", - "86c3dfc5-58d7-4ebd-a851-3b65a6bf5edd", - "620d5d7d-c452-498f-b848-b206a76891cd" - ], - "style": { - "scenario_layer": { - "color": "#e8ec18", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "YlOrRd"} - } - }, - { - "uuid": "43f96ed8-cd2f-4b91-b6c8-330d3b93bcc1", - "name": "Avoided Deforestation and Degradation", - "description": " This model focuses on preventing the conversion of forested areas into other land uses and minimizing degradation of existing forests. It involves implementing measures to protect and sustainably manage forests, preserving their biodiversity, carbon sequestration potential, and ecosystem services.", - "pwls_ids": [ - "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#ff4c84", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "RdPu"} - } - }, - { - "uuid": "c3c5a381-2b9f-4ddc-8a77-708239314fb6", - "name": "Avoided Wetland Conversion/Restoration", - "description": " This model aims to prevent the conversion of wetland ecosystems into other land uses and, where possible, restore degraded wetlands. It involves implementing conservation measures, such as land-use planning, regulatory frameworks, and restoration efforts, to safeguard the ecological functions and biodiversity of wetland habitats", - "pwls_ids": [ - "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#1f31d3", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "Blues"} - } - }, - { - "uuid": "3defbd0e-2b12-4ab2-a7d4-a035152396a7", - "name": "Bioproducts", - "description": " The bioproducts model focuses on utilizing natural resources sustainably to create value-added products. It involves the development and production of renewable and biodegradable materials, such as biofuels, bio-based chemicals, and bio-based materials, to reduce reliance on fossil fuels and promote a more sustainable economy.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "fef3c7e4-0cdf-477f-823b-a99da42f931e", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "fb92cac1-7744-4b11-8238-4e1da97650e0", - "9e5cff3f-73e7-4734-b76a-2a9f0536fa27", - "c5b1b81e-e1ae-41ec-adeb-7388f7597156", - "3872be6d-f791-41f7-b031-b85173e41d5e" - ], - "style": { - "scenario_layer": { - "color": "#67593f", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "BrBG"} - } - }, - { - "uuid": "22f9e555-0356-4b18-b292-c2d516dcdba5", - "name": "Bush Thinning", - "description": "Bush thinning refers to the controlled removal of excess woody vegetation in certain ecosystems and using that biomass to brush pack bare soil areas to promote regrowth of grass. This practice helps restore natural balance, prevent overgrowth, and enhance biodiversity.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "fef3c7e4-0cdf-477f-823b-a99da42f931e", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "e1a801c5-7f77-4746-be34-0138b62ff25c", - "478b0729-a507-4729-b1e4-b2bea7e161fd", - "5f329f53-31ff-4039-b0ec-a8d174a50866", - "5bcebbe2-7035-4d81-9817-0b4db8aa63e2" - ], - "style": { - "scenario_layer": { - "color": "#30ff01", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "BuGn"} - } - }, - { - "uuid": "177f1f27-cace-4f3e-9c3c-ef2cf54fc283", - "name": "Direct Tree Seeding", - "description": " This model involves planting tree seeds directly into the ground, allowing them to grow and establish without the need for nursery cultivation. It is a cost-effective and environmentally friendly approach to reforestation and afforestation efforts, promoting forest restoration and carbon sequestration.", - "pwls_ids": [ - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#bd6b70", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "PuRd"} - } - }, - { - "uuid": "d9d00a77-3db1-4390-944e-09b27bcbb981", - "name": "Livestock Rangeland Management", - "description": "This model focuses on sustainable management practices for livestock grazing on rangelands. It includes rotational grazing, monitoring of vegetation health, and implementing grazing strategies that promote biodiversity, soil health, and sustainable land use.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "fee0b421-805b-4bd9-a629-06586a760405", - "38a33633-9198-4b55-a424-135a4d522973", - "88dc8ff3-e61f-4a48-8f9b-5791efb6603f", - "a1bfff8e-fb87-4bca-97fa-a984d9bde712" - ], - "style": { - "scenario_layer": { - "color": "#ffa500", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "YlOrBr"} - } - }, - { - "uuid": "4fbfcb1c-bfd7-4305-b216-7a1077a2ccf7", - "name": "Livestock Market Access", - "description": " This model aims to improve market access for livestock producers practicing sustainable and regenerative farming methods. It involves creating networks, certifications, and partnerships that support the sale of sustainably produced livestock products, promoting economic viability and incentivizing environmentally friendly practices.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#6c0009", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "PRGn"} - } - }, - { - "uuid": "20491092-e665-4ee7-b92f-b0ed864c7312", - "name": "Natural Woodland Livestock Management", - "description": " This model emphasizes the sustainable management of livestock within natural woodland environments. It involves implementing practices that balance livestock grazing with the protection and regeneration of native woodlands, ensuring ecological integrity while meeting livestock production goals.", - "pwls_ids": [ - "c931282f-db2d-4644-9786-6720b3ab206a", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#007018", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "Greens"} - } - }, - { - "uuid": "1334cc3b-cb7b-46a3-923a-45d9b18d9d56", - "name": "Protected Area Expansion", - "description": "This model involves expanding existing protected areas to protect more grassland, savanna, and forest from converting to an anthropogenic land cover class.", - "pwls_ids": [ - "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#c27ba0", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "PiYG"} - } - }, - { - "uuid": "1334cc3b-cb7b-46a3-923a-45d9b18d9d56", - "name": "Protected Area Expansion", - "description": "This model involves expanding existing protected areas to protect more grassland, savanna, and forest from converting to an anthropogenic land cover class.", - "pwls_ids": [ - "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20" - ], - "style": { - "scenario_layer": { - "color": "#c27ba0", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "PiYG"} - } - }, - { - "uuid": "92054916-e8ea-45a0-992c-b6273d1b75a7", - "name": "Sustainable Crop Farming & Aquaponics", - "description": " This model combines sustainable crop farming practices such as agroecology, Permaculture and aquaponics, a system that integrates aquaculture (fish farming) with hydroponics (soil-less crop cultivation). It enables the production of crops with sustainable practices in a mutually beneficial and resource-efficient manner, reducing water usage and chemical inputs while maximizing productivity.", - "pwls_ids": [ - "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "fce41934-5196-45d5-80bd-96423ff0e74e", - "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "2f76304a-bb73-442c-9c02-ff9945389a20", - "6f7c1494-f73e-4e5e-8411-59676f9fa6e1", - "151668e7-8ffb-4766-9534-09949ab0356b", - "ed1ee71b-e7db-4599-97a9-a97c941a615f", - "307df1f4-206b-4f70-8db4-6505948e2a4e" - ], - "style": { - "scenario_layer": { - "color": "#781a8b", - "style": "solid", - "outline_width": "0", - "outline_color": "35,35,35,0" - }, - "activity_layer": {"color_ramp": "Purples"} - } - } - ] - } - \ No newline at end of file diff --git a/django_project/cplus/data/default/ncs_pathways.json b/django_project/cplus/data/default/ncs_pathways.json deleted file mode 100644 index e804d52..0000000 --- a/django_project/cplus/data/default/ncs_pathways.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "pathways": [ - { - "uuid": "b187f92f-b85b-45c4-9179-447f7ea114e3", - "name": "Agroforestry", - "description": "Provides additional carbon sequestration in agricultural systems by strategically planting trees in croplands.", - "path": "Final_Agroforestry_Priority_norm.tif", - "layer_type": 0, - "carbon_paths": ["bou_SOC_carbonsum_norm_inverse_null_con_clip.tif"] - }, - { - "uuid": "5fe775ba-0e80-4b70-a53a-1ed874b72da3", - "name": "Alien Plant Removal", - "description": "Alien Plant Class.", - "path": "Final_Alien_Invasive_Plant_priority_norm.tif", - "layer_type": 0, - "carbon_paths": [] - }, - { - "uuid": "bd381140-64f0-43d0-be6c-50120dd6c174", - "name": "Animal Management", - "description": "Provides additional soil carbon sequestration, reduces methane emissions from ruminants, and improves feed efficiency.", - "path": "Final_Animal_Management_Priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_null_con_clip.tif", - "SOC_trend_30m_4_scaled_clip_norm_inverse_null_con_clip.tif" - ] - }, - { - "uuid": "fc36dd06-aea3-4067-9626-2d73916d79b0", - "name": "Avoided Deforestation", - "description": "Avoids carbon emissions by preventing forest conversion in areas with a high risk of deforestation. Forest is defined as indigenous forest regions with tree density exceeding 75% with a canopy over 6m.", - "path": "Final_Avoided_Indigenous_Forest_priority_norm.tif", - "layer_type": 0, - "carbon_paths": ["bou_SOC_carbonsum_norm_null_con_clip.tif"] - }, - { - "uuid": "f7084946-6617-4c5d-97e8-de21059ca0d2", - "name": "Avoided Grassland Conversion", - "description": "Avoids carbon emissions by preventing the conversion of grasslands in areas with a high risk of grassland loss. Grassland is defined as regions with vegetation density less than 10%.", - "path": "Final_Avoided_Grassland_priority_norm.tif", - "layer_type": 0, - "carbon_paths": ["bou_SOC_carbonsum_norm_null_con_clip.tif"] - }, - { - "uuid": "00db44cf-a2e7-428a-86bb-0afedb9719ec", - "name": "Avoided Savanna Woodland Conversion", - "description": "Avoids carbon emissions by preventing the conversion of open woodland in areas with a high risk of open woodland loss. Savanna woodland is defined as savanna regions with open woodlands (vegetation density less than 35% and a tree canopy greater than 2.5m) and natural wooded lands (vegetation density greater than 35% and a tree canopy between 2.5m and 6m).", - "path": "Final_Avoided_OpenWoodland_NaturalWoodedland_priority_norm.tif", - "layer_type": 0, - "carbon_paths": ["bou_SOC_carbonsum_norm_null_con_clip.tif"] - }, - { - "uuid": "7228ecae-8759-448d-b7ea-19366f74ee02", - "name": "Avoided Wetland Conversion", - "description": "Avoids carbon emissions by preventing the conversion of wetlands in areas with a high risk of wetland loss. Wetlands are defined as natural or semi-natural wetlands covered in permanent or seasonal herbaceous vegetation.", - "path": "Final_Avoided_Wetland_priority_norm.tif", - "layer_type": 0, - "carbon_paths": ["bou_SOC_carbonsum_norm_null_con_clip.tif"] - }, - { - "uuid": "5475dd4a-5efc-4fb4-ae90-68ff4102591e", - "name": "Fire Management", - "description": "Provides additional sequestration and avoids carbon emissions by increasing resilience to catastrophic fire.", - "path": "Final_Fire_Management_Priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_null_con_clip.tif", - "SOC_trend_30m_4_scaled_clip_norm_inverse_null_con_clip.tif" - ] - }, - { - "uuid": "bede344c-9317-4c3f-801c-3117cc76be2c", - "name": "Restoration - Forest", - "description": "Provides additional carbon sequestration by converting non-forest into forest in areas where forests are the native cover type. This pathway excludes afforestation, where native non-forest areas are converted to forest. Forest is defined as indigenous forest regions with tree density exceeding 75% with a canopy over 6m.", - "path": "Final_Forest_Restoration_priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_inverse_null_con_clip.tif", - "SOC_trend_30m_4_scaled_clip_norm_inverse_null_con_clip.tif" - ] - }, - { - "uuid": "384863e3-08d1-453b-ac5f-94ad6a6aa1fd", - "name": "Restoration - Savanna", - "description": "Sequesters carbon through the restoration of native grassland and open woodland habitat. This pathway excludes the opportunity to convert non-native savanna regions to savannas. Savanna in this context contains grasslands (vegetation density less than 10%), open woodlands (vegetation density less than 35% and a tree canopy greater than 2.5m), and natural wooded lands (vegetation density greater than 75% and a tree canopy between 2.5m and 6m).", - "path": "Final_Sananna_Restoration_priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_inverse_null_con_clip.tif", - "SOC_trend_30m_4_scaled_clip_norm_inverse_null_con_clip.tif" - ] - }, - { - "uuid": "540470c7-0ed8-48af-8d91-63c15e6d69d7", - "name": "Restoration - Wetland", - "description": "Sequesters carbon through the restoration of wetland habitat. This pathway excludes the opportunity to convert non-native wetland regions to wetlands. Wetlands are defined as natural or semi-natural wetlands covered in permanent or seasonal herbaceous vegetation.", - "path": "Final_Wetland_Restoration_priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_inverse_null_con_clip.tif", - "SOC_trend_30m_4_scaled_clip_norm_inverse_null_con_clip.tif" - ] - }, - { - "uuid": "e6d7d4cd-dd6b-4ad5-b8a6-eab5436a89f1", - "name": "Sustainable Agriculture Crop Farming", - "description": "Change from natural grassland, woodland, forest in 1990 into cropland.", - "path": "Final_Sustinable_Ag_Crop_Farming_priority_norm.tif", - "layer_type": 0, - "carbon_paths": [ - "bou_SOC_carbonsum_norm_null_con_clip.tif" - ] - }, - { - "uuid": "71de0448-46c4-4163-a124-3d88cdcbba42", - "name": "Woody Encroachment Control", - "description": "Gradual woody plant encroachment into non-forest biomes has important negative consequences for ecosystem functioning, carbon balances, and economies.", - "path": "Final_woody_encroachment_norm.tif", - "layer_type": 0 - } - ] - } - \ No newline at end of file diff --git a/django_project/cplus/data/default/priority_weighting_layers.json b/django_project/cplus/data/default/priority_weighting_layers.json deleted file mode 100644 index 2c739d8..0000000 --- a/django_project/cplus/data/default/priority_weighting_layers.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "layers" :[ - { - "uuid": "c931282f-db2d-4644-9786-6720b3ab206a", - "name": "Social norm", - "description": "Placeholder text for social norm ", - "selected": true, - "path": "social_int_clip_norm.tif" - }, - { - "uuid": "f5687ced-af18-4cfc-9bc3-8006e40420b6", - "name": "Social norm inverse", - "description": "Placeholder text for social norm inverse", - "selected": false, - "path": "social_int_clip_norm_inverse.tif" - }, - { - "uuid": "fef3c7e4-0cdf-477f-823b-a99da42f931e", - "name": "Climate Resilience norm inverse", - "description": "Placeholder text for climate resilience", - "selected": false, - "path": "cccombo_clip_norm_inverse.tif" - }, - { - "uuid": "fce41934-5196-45d5-80bd-96423ff0e74e", - "name": "Climate Resilience norm", - "description": "Placeholder text for climate resilience norm", - "selected": false, - "path": "cccombo_clip_norm.tif" - }, - { - "uuid": "88c1c7dd-c5d1-420c-a71c-a5c595c1c5be", - "name": "Ecological Infrastructure", - "description": "Placeholder text for ecological infrastructure", - "selected": false, - "path": "ei_all_gknp_clip_norm.tif" - }, - { - "uuid": "3e0c7dff-51f2-48c5-a316-15d9ca2407cb", - "name": "Ecological Infrastructure inverse", - "description": "Placeholder text for ecological infrastructure inverse", - "selected": false, - "path": "ei_all_gknp_clip_norm.tif" - }, - { - "uuid": "9ab8c67a-5642-4a09-a777-bd94acfae9d1", - "name": "Biodiversity norm", - "description": "Placeholder text for biodiversity norm", - "selected": false, - "path": "biocombine_clip_norm.tif" - }, - { - "uuid": "c2dddd0f-a430-444a-811c-72b987b5e8ce", - "name": "Biodiversity norm inverse", - "description": "Placeholder text for biodiversity norm inverse", - "selected": false, - "path": "biocombine_clip_norm_inverse.tif" - } - ] -} diff --git a/django_project/cplus/data/layers/null_raster.tif b/django_project/cplus/data/layers/null_raster.tif deleted file mode 100644 index 50437df53ad5be7704486e80f4bdbe25561fd06b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442282 zcmeIuu?@m75J1s$f~*KB1xP?cK?@^fgKWX3S&d8N!Xr?;6xNq>I$3XBKQV1Jd;|y( zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009F3 zEigxvF-ku2n@4HAWDl)h`EP8!&(*HaU0=%S*fZtKwY$G{eQ&+*f3*47YCOA{clMAw I@-@f#18GkLW&i*H diff --git a/django_project/cplus/data/reports/main.qpt b/django_project/cplus/data/reports/main.qpt deleted file mode 100644 index 24072dc..0000000 --- a/django_project/cplus/data/reports/main.qpt +++ /dev/null @@ -1,2878 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
-
- - - - - - - - - - - - - -
- - - - - - -
diff --git a/django_project/cplus/definitions/__init__.py b/django_project/cplus/definitions/__init__.py deleted file mode 100644 index 833805c..0000000 --- a/django_project/cplus/definitions/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .constants import * # noqa -from .defaults import * # noqa diff --git a/django_project/cplus/definitions/constants.py b/django_project/cplus/definitions/constants.py deleted file mode 100644 index 789e3a3..0000000 --- a/django_project/cplus/definitions/constants.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Definitions for application constants. -""" - - -NCS_PATHWAY_SEGMENT = "ncs_pathways" -NCS_CARBON_SEGMENT = "ncs_carbon" -PRIORITY_LAYERS_SEGMENT = "priority_layers" -NPV_PRIORITY_LAYERS_SEGMENT = "npv" - -# Naming for outputs sub-folder relative to base directory -OUTPUTS_SEGMENT = "outputs" - -ACTIVITY_GROUP_LAYER_NAME = "Activity Maps" -ACTIVITY_WEIGHTED_GROUP_NAME = "Weighted Activity Maps" -NCS_PATHWAYS_GROUP_LAYER_NAME = "NCS Pathways Maps" - -# Attribute names -CARBON_COEFFICIENT_ATTRIBUTE = "carbon_coefficient" -CARBON_PATHS_ATTRIBUTE = "carbon_paths" -COLOR_RAMP_PROPERTIES_ATTRIBUTE = "color_ramp" -COLOR_RAMP_TYPE_ATTRIBUTE = "ramp_type" -DESCRIPTION_ATTRIBUTE = "description" -ACTIVITY_LAYER_STYLE_ATTRIBUTE = "activity_layer" -ACTIVITY_SCENARIO_STYLE_ATTRIBUTE = "scenario_layer" -LAYER_TYPE_ATTRIBUTE = "layer_type" -NAME_ATTRIBUTE = "name" -PATH_ATTRIBUTE = "path" -PATHWAYS_ATTRIBUTE = "pathways" -PIXEL_VALUE_ATTRIBUTE = "style_pixel_value" -STYLE_ATTRIBUTE = "style" -USER_DEFINED_ATTRIBUTE = "user_defined" -UUID_ATTRIBUTE = "uuid" -YEARS_ATTRIBUTE = "years" -DISCOUNT_ATTRIBUTE = "discount" -ABSOLUTE_NPV_ATTRIBUTE = "absolute_npv" -NORMALIZED_NPV_ATTRIBUTE = "normalized_npv" -YEARLY_RATES_ATTRIBUTE = "yearly_rates" -ENABLED_ATTRIBUTE = "enabled" -MIN_VALUE_ATTRIBUTE = "minimum_value" -MAX_VALUE_ATTRIBUTE = "maximum_value" -COMPUTED_ATTRIBUTE = "use_computed" -NPV_MAPPINGS_ATTRIBUTE = "mappings" -REMOVE_EXISTING_ATTRIBUTE = "remove_existing" - -ACTIVITY_IDENTIFIER_PROPERTY = "activity_identifier" -NPV_COLLECTION_PROPERTY = "npv_collection" - -# Option / settings keys -CPLUS_OPTIONS_KEY = "cplus_main" -LOG_OPTIONS_KEY = "cplus_log" -REPORTS_OPTIONS_KEY = "cplus_report" - -# Headers for financial NPV computation -YEAR_HEADER = "Year" -TOTAL_PROJECTED_COSTS_HEADER = "Projected Total Costs/ha (US$)" -TOTAL_PROJECTED_REVENUES_HEADER = "Projected Total Revenues/ha (US$)" -DISCOUNTED_VALUE_HEADER = "Discounted Value (US$)" -MAX_YEARS = 99 - -NO_DATA_VALUE = -9999 \ No newline at end of file diff --git a/django_project/cplus/definitions/defaults.py b/django_project/cplus/definitions/defaults.py deleted file mode 100644 index 5956e1c..0000000 --- a/django_project/cplus/definitions/defaults.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Definitions for all defaults settings -""" - -import os -import json - -from pathlib import Path - -PILOT_AREA_EXTENT = { - "type": "Polygon", - "coordinates": [30.743498637, 32.069186664, -25.201606226, -23.960197335], -} - -DEFAULT_CRS_ID = 4326 - -DOCUMENTATION_SITE = "https://conservationinternational.github.io/cplus-plugin" -USER_DOCUMENTATION_SITE = ( - "https://conservationinternational.github.io/cplus-plugin/user/guide" -) -ABOUT_DOCUMENTATION_SITE = ( - "https://conservationinternational.github.io/cplus-plugin/about/ci" -) -REPORT_DOCUMENTATION = "https://conservationinternational.github.io/cplus-plugin/user/guide/#report-generating" - -OPTIONS_TITLE = "CPLUS" # Title in the QGIS settings -GENERAL_OPTIONS_TITLE = "General" -REPORT_OPTIONS_TITLE = "Reporting" -LOG_OPTIONS_TITLE = "Logs" -ICON_PATH = ":/plugins/cplus_plugin/icon.svg" -REPORT_SETTINGS_ICON_PATH = str( - os.path.normpath( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/report_settings.svg" - ) -) -LOG_SETTINGS_ICON_PATH = str( - os.path.normpath( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/log_settings.svg" - ) -) -ICON_PDF = ( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/mActionSaveAsPDF.svg" -) -ICON_LAYOUT = ( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/mActionNewLayout.svg" -) -ICON_REPORT = ( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/mIconReport.svg" -) -ICON_HELP = ( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/mActionHelpContents_green.svg" -) - -ADD_LAYER_ICON_PATH = ":/plugins/cplus_plugin/cplus_left_arrow.svg" -REMOVE_LAYER_ICON_PATH = ":/plugins/cplus_plugin/cplus_right_arrow.svg" - -SCENARIO_OUTPUT_FILE_NAME = "cplus_scenario_output" -SCENARIO_OUTPUT_LAYER_NAME = "scenario_result" - -PILOT_AREA_SCENARIO_SYMBOLOGY = { - "Agroforestry": {"val": 1, "color": "#d80007"}, - "Alien Plant Removal": {"val": 2, "color": "#6f6f6f"}, - "Applied Nucleation": {"val": 3, "color": "#81c4ff"}, - "Assisted Natural Regeneration": {"val": 4, "color": "#e8ec18"}, - "Avoided Deforestation and Degradation": {"val": 5, "color": "#ff4c84"}, - "Avoided Wetland Conversion/Restoration": {"val": 6, "color": "#1f31d3"}, - "Bioproducts": {"val": 7, "color": "#67593f"}, - "Bush Thinning": {"val": 8, "color": "#30ff01"}, - "Direct Tree Seeding": {"val": 9, "color": "#bd6b70"}, - "Livestock Market Access": {"val": 10, "color": "#6c0009"}, - "Livestock Rangeland Management": {"val": 11, "color": "#ffa500"}, - "Natural Woodland Livestock Management": {"val": 12, "color": "#007018"}, - "Sustainable Crop Farming & Aquaponics": {"val": 13, "color": "#781a8b"}, -} - -ACTIVITY_COLOUR_RAMPS = { - "Agroforestry": "Reds", - "Alien Plant Removal": "Greys", - "Alien_Plant_Removal": "Greys", - "Applied Nucleation": "PuBu", - "Applied_Nucleation": "PuBu", - "Assisted Natural Regeneration": "YlOrRd", - "Assisted_Natural_Regeneration": "YlOrRd", - "Avoided Deforestation and Degradation": "RdPu", - "Avoided_Deforestation_and_Degradation": "RdPu", - "Avoided Wetland Conversion/Restoration": "Blues", - "Avoided_Wetland_Conversion_Restoration": "Blues", - "Bioproducts": "BrBG", - "Bush Thinning": "BuGn", - "Bush_Thinning": "BuGn", - "Direct Tree Seeding": "PuRd", - "Direct_Tree_Seeding": "PuRd", - "Livestock Market Access": "Rocket", - "Livestock_Market_Access": "Rocket", - "Livestock Rangeland Management": "YlOrBr", - "Livestock_Rangeland_Management": "YlOrBr", - "Natural Woodland Livestock Management": "Greens", - "Natural_Woodland_Livestock_Management": "Greens", - "Sustainable Crop Farming & Aquaponics": "Purples", - "Sustainable_Crop_Farming_&_Aquaponics": "Purples", -} - -QGIS_GDAL_PROVIDER = "gdal" - -DEFAULT_LOGO_PATH = ( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + "/icons/ci_logo.png" -) -CPLUS_LOGO_PATH = str( - os.path.normpath( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/cplus_logo.svg" - ) -) -CI_LOGO_PATH = str( - os.path.normpath( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - + "/icons/ci_logo.svg" - ) -) - -# Default template file name -TEMPLATE_NAME = "main.qpt" - -# Minimum sizes (in mm) for repeat items in the template -MINIMUM_ITEM_WIDTH = 100 -MINIMUM_ITEM_HEIGHT = 100 - -# Report font -REPORT_FONT_NAME = "Ubuntu" - -# IDs for the given tables in the report template -ACTIVITY_AREA_TABLE_ID = "activity_area_table" -PRIORITY_GROUP_WEIGHT_TABLE_ID = "assigned_weights_table" - -# Initiliazing the plugin default data as found in the data directory -priority_layer_path = ( - Path(__file__).parent.parent.resolve() - / "data" - / "default" - / "priority_weighting_layers.json" -) - -with priority_layer_path.open("r") as fh: - priority_layers_dict = json.load(fh) -PRIORITY_LAYERS = priority_layers_dict["layers"] - - -pathways_path = ( - Path(__file__).parent.parent.resolve() / "data" / "default" / "ncs_pathways.json" -) - -with pathways_path.open("r") as fh: - pathways_dict = json.load(fh) -# Path just contains the file name and is relative to {download_folder}/ncs_pathways -DEFAULT_NCS_PATHWAYS = pathways_dict["pathways"] - - -activities_path = ( - Path(__file__).parent.parent.resolve() / "data" / "default" / "activities.json" -) - -with activities_path.open("r") as fh: - models_dict = json.load(fh) - -DEFAULT_ACTIVITIES = models_dict["activities"] - - -PRIORITY_GROUPS = [ - { - "uuid": "dcfb3214-4877-441c-b3ef-8228ab6dfad3", - "name": "Biodiversity", - "description": "Placeholder text for bio diversity", - }, - { - "uuid": "8b9fb419-b6b8-40e8-9438-c82901d18cd9", - "name": "Livelihood", - "description": "Placeholder text for livelihood", - }, - { - "uuid": "21a30a80-eb49-4c5e-aff6-558123688e09", - "name": "Climate Resilience", - "description": "Placeholder text for climate resilience ", - }, - { - "uuid": "ae1791c3-93fd-4e8a-8bdf-8f5fced11ade", - "name": "Ecological infrastructure", - "description": "Placeholder text for ecological infrastructure", - }, - { - "uuid": "8cac9e25-98a8-4eae-a257-14a4ef8995d0", - "name": "Policy", - "description": "Placeholder text for policy", - }, - { - "uuid": "3a66c845-2f9b-482c-b9a9-bcfca8395ad5", - "name": "Finance - Years Experience", - "description": "Placeholder text for years of experience", - }, - { - "uuid": "c6dbfe09-b05c-4cfc-8fc0-fb63cfe0ceee", - "name": "Finance - Market Trends", - "description": "Placeholder text for market trends", - }, - { - "uuid": "3038cce0-3470-4b09-bb2a-f82071fe57fd", - "name": "Finance - Net Present value", - "description": "Placeholder text for net present value", - }, - { - "uuid": "3b2c7421-f879-48ef-a973-2aa3b1390694", - "name": "Finance - Carbon", - "description": "Placeholder text for finance carbon", - }, -] - -DEFAULT_REPORT_DISCLAIMER = ( - "The boundaries, names, and designations " - "used in this report do not imply official " - "endorsement or acceptance by Conservation " - "International Foundation, or its partner " - "organizations and contributors." -) -DEFAULT_REPORT_LICENSE = ( - "Creative Commons Attribution 4.0 International " "License (CC BY 4.0)" -) diff --git a/django_project/cplus/migrations/__init__.py b/django_project/cplus/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_project/cplus/models/__init__.py b/django_project/cplus/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_project/cplus/models/base.py b/django_project/cplus/models/base.py deleted file mode 100644 index 0522b62..0000000 --- a/django_project/cplus/models/base.py +++ /dev/null @@ -1,586 +0,0 @@ -# -*- coding: utf-8 -*- - -""" QGIS CPLUS plugin models. -""" - -import dataclasses -import datetime -from enum import Enum, IntEnum -import os.path -import typing -from uuid import UUID - -from qgis.core import ( - QgsColorBrewerColorRamp, - QgsColorRamp, - QgsCptCityColorRamp, - QgsFillSymbol, - QgsGradientColorRamp, - QgsLimitedRandomColorRamp, - QgsMapLayer, - QgsPresetSchemeColorRamp, - QgsRandomColorRamp, - QgsRasterLayer, - QgsVectorLayer, -) - -from cplus.definitions.constants import ( - COLOR_RAMP_PROPERTIES_ATTRIBUTE, - COLOR_RAMP_TYPE_ATTRIBUTE, - ACTIVITY_LAYER_STYLE_ATTRIBUTE, - ACTIVITY_SCENARIO_STYLE_ATTRIBUTE, -) - - -@dataclasses.dataclass -class SpatialExtent: - """Extent object that stores - the coordinates of the area of interest - """ - - bbox: typing.List[float] - - -class PRIORITY_GROUP(Enum): - """Represents priority groups types""" - - CARBON_IMPORTANCE = "Carbon importance" - BIODIVERSITY = "Biodiversity" - LIVELIHOOD = "Livelihood" - CLIMATE_RESILIENCE = "Climate Resilience" - ECOLOGICAL_INFRASTRUCTURE = "Ecological infrastructure" - POLICY = "Policy" - FINANCE_YEARS_EXPERIENCE = "Finance - Years Experience" - FINANCE_MARKET_TRENDS = "Finance - Market Trends" - FINANCE_NET_PRESENT_VALUE = "Finance - Net Present value" - FINANCE_CARBON = "Finance - Carbon" - - -@dataclasses.dataclass -class BaseModelComponent: - """Base class for common model item properties.""" - - uuid: UUID - name: str - description: str - - def __eq__(self, other: "BaseModelComponent") -> bool: - """Test equality of object with another BaseModelComponent - object using the attributes. - - :param other: BaseModelComponent object to compare with this object. - :type other: BaseModelComponent - - :returns: True if the all the attribute values match, else False. - :rtype: bool - """ - if self.uuid != other.uuid: - return False - - if self.name != other.name: - return False - - if self.description != other.description: - return False - - return True - - -BaseModelComponentType = typing.TypeVar( - "BaseModelComponentType", bound=BaseModelComponent -) - - -class LayerType(IntEnum): - """QGIS spatial layer type.""" - - RASTER = 0 - VECTOR = 1 - UNDEFINED = -1 - - -class ModelComponentType(Enum): - """Type of model component i.e. NCS pathway or - activity. - """ - - NCS_PATHWAY = "ncs_pathway" - ACTIVITY = "activity" - UNKNOWN = "unknown" - - @staticmethod - def from_string(str_enum: str) -> "ModelComponentType": - """Creates an enum from the corresponding string equivalent. - - :param str_enum: String representing the model component type. - :type str_enum: str - - :returns: Component type enum corresponding to the given - string else unknown if not found. - :rtype: ModelComponentType - """ - if str_enum.lower() == "ncs_pathway": - return ModelComponentType.NCS_PATHWAY - elif str_enum.lower() == "activity": - return ModelComponentType.ACTIVITY - - return ModelComponentType.UNKNOWN - - -@dataclasses.dataclass -class LayerModelComponent(BaseModelComponent): - """Base class for model components that support - a map layer. - """ - - path: str = "" - layer_type: LayerType = LayerType.UNDEFINED - user_defined: bool = False - - def __post_init__(self): - """Try to set the layer and layer type properties.""" - self.update_layer_type() - - def update_layer_type(self): - """Update the layer type if either the layer or - path properties have been set. - """ - layer = self.to_map_layer() - if layer is None: - return - - if not layer.isValid(): - return - - if isinstance(layer, QgsRasterLayer): - self.layer_type = LayerType.RASTER - - elif isinstance(layer, QgsVectorLayer): - self.layer_type = LayerType.VECTOR - - def to_map_layer(self) -> typing.Union[QgsMapLayer, None]: - """Constructs a map layer from the specified path. - - It will first check if the layer property has been set - else try to construct the layer from the path else return - None. - - :returns: Map layer corresponding to the set layer - property or specified path. - :rtype: QgsMapLayer - """ - if not os.path.exists(self.path): - return None - - layer = None - if self.layer_type == LayerType.RASTER: - layer = QgsRasterLayer(self.path, self.name) - - elif self.layer_type == LayerType.VECTOR: - layer = QgsVectorLayer(self.path, self.name) - - return layer - - def is_valid(self) -> bool: - """Checks if the corresponding map layer is valid. - - :returns: True if the map layer is valid, else False if map layer is - invalid or of None type. - :rtype: bool - """ - layer = self.to_map_layer() - if layer is None: - return False - - return layer.isValid() - - def __eq__(self, other) -> bool: - """Uses BaseModelComponent equality test rather than - what the dataclass default implementation will provide. - """ - return super().__eq__(other) - - -LayerModelComponentType = typing.TypeVar( - "LayerModelComponentType", bound=LayerModelComponent -) - -class PriorityLayerType(IntEnum): - """Type of priority weighting layer.""" - - DEFAULT = 0 - NPV = 1 - - -@dataclasses.dataclass -class PriorityLayer(BaseModelComponent): - """Base class for model components storing priority weighting layers.""" - - groups: list - selected: bool = False - path: str = "" - type: PriorityLayerType = PriorityLayerType.DEFAULT - - -@dataclasses.dataclass -class NcsPathway(LayerModelComponent): - """Contains information about an NCS pathway layer.""" - - carbon_paths: typing.List[str] = dataclasses.field(default_factory=list) - - def __eq__(self, other: "NcsPathway") -> bool: - """Test equality of NcsPathway object with another - NcsPathway object using the attributes. - - Excludes testing the map layer for equality. - - :param other: NcsPathway object to compare with this object. - :type other: NcsPathway - - :returns: True if all the attribute values match, else False. - :rtype: bool - """ - base_equality = super().__eq__(other) - if not base_equality: - return False - - if self.path != other.path: - return False - - if self.layer_type != other.layer_type: - return False - - if self.user_defined != other.user_defined: - return False - - return True - - def add_carbon_path(self, carbon_path: str) -> bool: - """Add a carbon layer path. - - Checks if the path has already been defined or if it exists - in the file system. - - :returns: True if the carbon layer path was successfully - added, else False if the path has already been defined - or does not exist in the file system. - :rtype: bool - """ - if carbon_path in self.carbon_paths: - return False - - if not os.path.exists(carbon_path): - return False - - self.carbon_paths.append(carbon_path) - - return True - - def carbon_layers(self) -> typing.List[QgsRasterLayer]: - """Returns the list of carbon layers whose path is defined under - the :py:attr:`~carbon_paths` attribute. - - The caller should check the validity of the layers or use - :py:meth:`~is_carbon_valid` function. - - :returns: Carbon layers for the NCS pathway or an empty list - if the path is not defined. - :rtype: list - """ - return [QgsRasterLayer(carbon_path) for carbon_path in self.carbon_paths] - - def is_carbon_valid(self) -> bool: - """Checks if the carbon layers are valid. - - :returns: True if all carbon layers are valid, else False if - even one is invalid. If there are no carbon layers defined, it will - always return True. - :rtype: bool - """ - is_valid = True - for cl in self.carbon_layers(): - if not cl.isValid(): - is_valid = False - break - - return is_valid - - def is_valid(self) -> bool: - """Additional check to include validity of carbon layers.""" - valid = super().is_valid() - if not valid: - return False - - carbon_valid = self.is_carbon_valid() - if not carbon_valid: - return False - - return True - - -@dataclasses.dataclass -class Activity(LayerModelComponent): - """Contains information about an activity used in a scenario. - If the layer has been set then it will - not be possible to add NCS pathways unless the layer - is cleared. - Priority will be given to the layer property. - """ - - pathways: typing.List[NcsPathway] = dataclasses.field(default_factory=list) - priority_layers: typing.List[typing.Dict] = dataclasses.field(default_factory=list) - layer_styles: dict = dataclasses.field(default_factory=dict) - style_pixel_value: int = -1 - - def __post_init__(self): - """Pre-checks on initialization.""" - super().__post_init__() - - # Ensure there are no duplicate pathways. - uuids = [str(p.uuid) for p in self.pathways] - - if len(set(uuids)) != len(uuids): - msg = "Duplicate pathways found in activity" - raise ValueError(f"{msg} {self.name}.") - - # Reset pathways if layer has also been set. - if self.to_map_layer() is not None and len(self.pathways) > 0: - self.pathways = [] - - def contains_pathway(self, pathway_uuid: str) -> bool: - """Checks if there is an NCS pathway matching the given UUID. - - :param pathway_uuid: UUID to search for in the collection. - :type pathway_uuid: str - - :returns: True if there is a matching NCS pathway, else False. - :rtype: bool - """ - ncs_pathway = self.pathway_by_uuid(pathway_uuid) - if ncs_pathway is None: - return False - - return True - - def add_ncs_pathway(self, ncs: NcsPathway) -> bool: - """Adds an NCS pathway object to the collection. - - :param ncs: NCS pathway to be added to the activity. - :type ncs: NcsPathway - - :returns: True if the NCS pathway was successfully added, else False - if there was an existing NCS pathway object with a similar UUID or - the layer property had already been set. - """ - - if not ncs.is_valid(): - return False - - if self.contains_pathway(str(ncs.uuid)): - return False - - self.pathways.append(ncs) - - return True - - def clear_layer(self): - """Removes a reference to the layer URI defined in the path attribute.""" - self.path = "" - - def remove_ncs_pathway(self, pathway_uuid: str) -> bool: - """Removes the NCS pathway with a matching UUID from the collection. - - :param pathway_uuid: UUID for the NCS pathway to be removed. - :type pathway_uuid: str - - :returns: True if the NCS pathway object was successfully removed, - else False if there is no object matching the given UUID. - :rtype: bool - """ - idxs = [i for i, p in enumerate(self.pathways) if str(p.uuid) == pathway_uuid] - - if len(idxs) == 0: - return False - - rem_idx = idxs[0] - _ = self.pathways.pop(rem_idx) - - return True - - def pathway_by_uuid(self, pathway_uuid: str) -> typing.Union[NcsPathway, None]: - """Returns an NCS pathway matching the given UUID. - - :param pathway_uuid: UUID for the NCS pathway to retrieve. - :type pathway_uuid: str - - :returns: NCS pathway object matching the given UUID else None if - not found. - :rtype: NcsPathway - """ - pathways = [p for p in self.pathways if str(p.uuid) == pathway_uuid] - - if len(pathways) == 0: - return None - - return pathways[0] - - def pw_layers(self) -> typing.List[QgsRasterLayer]: - """Returns the list of priority weighting layers defined under - the :py:attr:`~priority_layers` attribute. - - :returns: Priority layers for the implementation or an empty list - if the path is not defined. - :rtype: list - """ - return [QgsRasterLayer(layer.get("path")) for layer in self.priority_layers] - - def is_pwls_valid(self) -> bool: - """Checks if the priority layers are valid. - - :returns: True if all priority layers are valid, else False if - even one is invalid. If there are no priority layers defined, it will - always return True. - :rtype: bool - """ - is_valid = True - for cl in self.pw_layers(): - if not cl.isValid(): - is_valid = False - break - - return is_valid - - def is_valid(self) -> bool: - """Includes an additional check to assert if NCS pathways have - been specified if the layer has not been set or is not valid. - - Does not check for validity of individual NCS pathways in the - collection. - """ - if self.to_map_layer() is not None: - return super().is_valid() - else: - if len(self.pathways) == 0: - return False - - if not self.is_pwls_valid(): - return False - - return True - - def scenario_layer_style_info(self) -> dict: - """Returns the fill symbol properties for styling the activity - layer in the final scenario result. - - :returns: Fill symbol properties for the activity layer - styling in the scenario layer or an empty dictionary if there was - no definition found in the root style. - :rtype: dict - """ - if ( - len(self.layer_styles) == 0 - or ACTIVITY_SCENARIO_STYLE_ATTRIBUTE not in self.layer_styles - ): - return dict() - - return self.layer_styles[ACTIVITY_SCENARIO_STYLE_ATTRIBUTE] - - def activity_layer_style_info(self) -> dict: - """Returns the color ramp properties for styling the activity - layer resulting from a scenario run. - - :returns: Color ramp properties for the activity styling or an - empty dictionary if there was no definition found in the root - style. - :rtype: dict - """ - if ( - len(self.layer_styles) == 0 - or ACTIVITY_LAYER_STYLE_ATTRIBUTE not in self.layer_styles - ): - return dict() - - return self.layer_styles[ACTIVITY_LAYER_STYLE_ATTRIBUTE] - - def scenario_fill_symbol(self) -> typing.Union[QgsFillSymbol, None]: - """Creates a fill symbol for the activity in the scenario. - - :returns: Fill symbol for the activity in the scenario - or None if there was no definition found. - :rtype: QgsFillSymbol - """ - scenario_style_info = self.scenario_layer_style_info() - if len(scenario_style_info) == 0: - return None - - return QgsFillSymbol.createSimple(scenario_style_info) - - def color_ramp(self) -> typing.Union[QgsColorRamp, None]: - """Create a color ramp for styling the activity layer resulting - from a scenario run. - - :returns: A color ramp for styling the activity layer or None - if there was no definition found. - :rtype: QgsColorRamp - """ - model_layer_info = self.activity_layer_style_info() - if len(model_layer_info) == 0: - return None - - ramp_info = model_layer_info.get(COLOR_RAMP_PROPERTIES_ATTRIBUTE, None) - if ramp_info is None or len(ramp_info) == 0: - return None - - ramp_type = model_layer_info.get(COLOR_RAMP_TYPE_ATTRIBUTE, None) - if ramp_type is None: - return None - - # New ramp types will need to be added here manually - if ramp_type == QgsColorBrewerColorRamp.typeString(): - return QgsColorBrewerColorRamp.create(ramp_info) - elif ramp_type == QgsCptCityColorRamp.typeString(): - return QgsCptCityColorRamp.create(ramp_info) - elif ramp_type == QgsGradientColorRamp.typeString(): - return QgsGradientColorRamp.create(ramp_info) - elif ramp_type == QgsLimitedRandomColorRamp.typeString(): - return QgsLimitedRandomColorRamp.create(ramp_info) - elif ramp_type == QgsPresetSchemeColorRamp.typeString(): - return QgsPresetSchemeColorRamp.create(ramp_info) - elif ramp_type == QgsRandomColorRamp.typeString(): - return QgsRandomColorRamp() - - return None - - -class ScenarioState(Enum): - """Defines scenario analysis process states""" - - IDLE = 0 - RUNNING = 1 - STOPPED = 3 - FINISHED = 4 - TERMINATED = 5 - - -@dataclasses.dataclass -class Scenario(BaseModelComponent): - """Object for the handling - workflow scenario information. - """ - - extent: SpatialExtent - activities: typing.List[Activity] - weighted_activities: typing.List[Activity] - priority_layer_groups: typing.List - state: ScenarioState = ScenarioState.IDLE - - -@dataclasses.dataclass -class ScenarioResult: - """Scenario result details.""" - - scenario: Scenario - created_date: datetime.datetime = datetime.datetime.now() - analysis_output: typing.Dict = None - output_layer_name: str = "" - scenario_directory: str = "" diff --git a/django_project/cplus/models/financial.py b/django_project/cplus/models/financial.py deleted file mode 100644 index ad1ddf1..0000000 --- a/django_project/cplus/models/financial.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- - -""" Data models for the financial elements of the tool.""" - -import dataclasses -from enum import IntEnum -import typing - -from .base import Activity - - -@dataclasses.dataclass -class NpvParameters: - """Parameters for computing an activity's NPV.""" - - years: int - discount: float - absolute_npv: float = 0.0 - normalized_npv: float = 0.0 - # Each tuple contains 3 elements i.e. revenue, costs and discount rates - yearly_rates: typing.List[tuple] = dataclasses.field(default_factory=list) - - -@dataclasses.dataclass -class ActivityNpv: - """Mapping of the NPV parameters to the corresponding Activity model.""" - - params: NpvParameters - enabled: bool - activity: typing.Optional[Activity] - - @property - def activity_id(self) -> str: - """Gets the identifier of the activity model. - - :returns: The unique identifier of the activity model else an - empty string if no activity has been set. - """ - if not self.activity: - return "" - - return str(self.activity.uuid) - - @property - def base_name(self) -> str: - """Returns a proposed name for the activity NPV. - - An empty string will be return id the `activity` attribute - is not set. - - :returns: Proposed base name for the activity NPV. - :rtype: str - """ - if self.activity is None: - return "" - - return f"{self.activity.name} NPV Norm" - - -@dataclasses.dataclass -class ActivityNpvCollection: - """Collection for all ActivityNpvMapping configurations that have been - specified by the user. - """ - - minimum_value: float - maximum_value: float - use_computed: bool = True - remove_existing: bool = False - mappings: typing.List[ActivityNpv] = dataclasses.field(default_factory=list) - - def activity_npv(self, activity_identifier: str) -> typing.Optional[ActivityNpv]: - """Gets the mapping of an activity's NPV mapping if defined. - - :param activity_identifier: Unique identifier of an activity whose - NPV mapping is to be retrieved. - :type activity_identifier: str - - :returns: The activity's NPV mapping else None if not found. - :rtype: ActivityNpv - """ - matching_mapping = [ - activity_npv - for activity_npv in self.mappings - if activity_npv.activity_id == activity_identifier - ] - - return None if len(matching_mapping) == 0 else matching_mapping[0] - - def update_computed_normalization_range(self) -> bool: - """Update the minimum and maximum normalization values - based on the absolute values of the existing ActivityNpv - objects. - - Values for disabled activity NPVs will be excluded from - the computation. - - :returns: True if the min/max values were updated else False if - there are no mappings or valid absolute NPV values defined. - """ - if len(self.mappings) == 0: - return False - - valid_npv_values = [ - activity_npv.params.absolute_npv - for activity_npv in self.mappings - if activity_npv.params.absolute_npv is not None and activity_npv.enabled - ] - - if len(valid_npv_values) == 0: - return False - - self.minimum_value = min(valid_npv_values) - self.maximum_value = max(valid_npv_values) - - return True - - def normalize_npvs(self) -> bool: - """Normalize the NPV values of the activities using the specified - normalization range. - - If the absolute NPV values are less than or greater than the - normalization range, then they will be truncated to 0.0 and 1.0 - respectively. To avoid such a situation from occurring, it is recommended - to make sure that the ranges are synchronized using the latest absolute - NPV values hence call `update_computed_normalization_range` before - normalizing the NPVs. - - :returns: True if the NPVs were successfully normalized else False due - to various reasons such as if the minimum value is greater than the - maximum value or if the min/max values are the same. - """ - if self.minimum_value > self.maximum_value: - return False - - norm_range = float(self.maximum_value - self.minimum_value) - - if norm_range == 0.0: - return False - - for activity_npv in self.mappings: - absolute_npv = activity_npv.params.absolute_npv - if not absolute_npv: - continue - - if absolute_npv <= self.minimum_value: - normalized_npv = 0.0 - elif absolute_npv >= self.maximum_value: - normalized_npv = 1.0 - else: - normalized_npv = (absolute_npv - self.minimum_value) / norm_range - - activity_npv.params.normalized_npv = normalized_npv - - return True - - -@dataclasses.dataclass -class ActivityNpvPwl: - """Convenience class that contains parameters for creating - a PWL raster layer. - """ - - npv: ActivityNpv - extent: typing.List[float] - crs: str - pixel_size: float diff --git a/django_project/cplus/models/helpers.py b/django_project/cplus/models/helpers.py deleted file mode 100644 index 7c2c131..0000000 --- a/django_project/cplus/models/helpers.py +++ /dev/null @@ -1,594 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Helper functions for supporting model management.""" - -from dataclasses import field, fields -import typing -import uuid - -from .base import ( - BaseModelComponent, - BaseModelComponentType, - Activity, - LayerModelComponent, - LayerModelComponentType, - LayerType, - NcsPathway, - SpatialExtent, -) -from ..definitions.constants import ( - ACTIVITY_IDENTIFIER_PROPERTY, - ABSOLUTE_NPV_ATTRIBUTE, - CARBON_PATHS_ATTRIBUTE, - COMPUTED_ATTRIBUTE, - DISCOUNT_ATTRIBUTE, - ENABLED_ATTRIBUTE, - STYLE_ATTRIBUTE, - NAME_ATTRIBUTE, - DESCRIPTION_ATTRIBUTE, - LAYER_TYPE_ATTRIBUTE, - NPV_MAPPINGS_ATTRIBUTE, - MAX_VALUE_ATTRIBUTE, - MIN_VALUE_ATTRIBUTE, - NORMALIZED_NPV_ATTRIBUTE, - PATH_ATTRIBUTE, - PIXEL_VALUE_ATTRIBUTE, - PRIORITY_LAYERS_SEGMENT, - REMOVE_EXISTING_ATTRIBUTE, - USER_DEFINED_ATTRIBUTE, - UUID_ATTRIBUTE, - YEARS_ATTRIBUTE, - YEARLY_RATES_ATTRIBUTE, -) -from ..definitions.defaults import DEFAULT_CRS_ID -from .financial import ActivityNpv, ActivityNpvCollection, NpvParameters - -from ..utils.helper import log - -from qgis.core import ( - QgsCoordinateReferenceSystem, - QgsCoordinateTransform, - QgsProject, - QgsRectangle, -) - - -def model_component_to_dict( - model_component: BaseModelComponentType, uuid_to_str=True -) -> dict: - """Creates a dictionary containing the base attribute - name-value pairs from a model component object. - - :param model_component: Source model component object whose - values are to be mapped to the corresponding - attribute names. - :type model_component: BaseModelComponent - - :param uuid_to_str: Set True to convert the UUID to a - string equivalent, else False. Some serialization engines - such as JSON are unable to handle UUID objects hence the need - to convert to string. - :type uuid_to_str: bool - - :returns: Returns a dictionary item containing attribute - name-value pairs. - :rtype: dict - """ - model_uuid = model_component.uuid - if uuid_to_str: - model_uuid = str(model_uuid) - - return { - UUID_ATTRIBUTE: model_uuid, - NAME_ATTRIBUTE: model_component.name, - DESCRIPTION_ATTRIBUTE: model_component.description, - } - - -def create_model_component( - source_dict: dict, - model_cls: typing.Callable[[uuid.UUID, str, str], BaseModelComponentType], -) -> typing.Union[BaseModelComponentType, None]: - """Factory method for creating and setting attribute values - for a base model component object. - - :param source_dict: Dictionary containing attribute values. - :type source_dict: dict - - :param model_cls: Callable class that will be created based on the - input argument values from the dictionary. - :type model_cls: BaseModelComponent - - :returns: Base model component object with property values - derived from the dictionary. - :rtype: BaseModelComponent - """ - if not issubclass(model_cls, BaseModelComponent): - return None - - return model_cls( - uuid.UUID(source_dict[UUID_ATTRIBUTE]), - source_dict[NAME_ATTRIBUTE], - source_dict[DESCRIPTION_ATTRIBUTE], - ) - - -def create_layer_component( - source_dict, - model_cls: typing.Callable[ - [uuid.UUID, str, str, str, LayerType, bool], LayerModelComponentType - ], -) -> typing.Union[LayerModelComponent, None]: - """Factory method for creating a layer model component using - attribute values defined in a dictionary. - - :param source_dict: Dictionary containing property values. - :type source_dict: dict - - :param model_cls: Callable class that will be created based on the - input argument values from the dictionary. - :type model_cls: LayerModelComponent - - :returns: Layer model component object with property values set - from the dictionary. - :rtype: LayerModelComponent - """ - if UUID_ATTRIBUTE not in source_dict: - return None - - source_uuid = source_dict[UUID_ATTRIBUTE] - if isinstance(source_uuid, str): - source_uuid = uuid.UUID(source_uuid) - - kwargs = {} - if PATH_ATTRIBUTE in source_dict: - kwargs[PATH_ATTRIBUTE] = source_dict[PATH_ATTRIBUTE] - - if LAYER_TYPE_ATTRIBUTE in source_dict: - kwargs[LAYER_TYPE_ATTRIBUTE] = LayerType(int(source_dict[LAYER_TYPE_ATTRIBUTE])) - - if USER_DEFINED_ATTRIBUTE in source_dict: - kwargs[USER_DEFINED_ATTRIBUTE] = bool(source_dict[USER_DEFINED_ATTRIBUTE]) - - return model_cls( - source_uuid, - source_dict[NAME_ATTRIBUTE], - source_dict[DESCRIPTION_ATTRIBUTE], - **kwargs, - ) - - -def create_ncs_pathway(source_dict) -> typing.Union[NcsPathway, None]: - """Factory method for creating an NcsPathway object using - attribute values defined in a dictionary. - - :param source_dict: Dictionary containing property values. - :type source_dict: dict - - :returns: NCS pathway object with property values set - from the dictionary. - :rtype: NcsPathway - """ - ncs = create_layer_component(source_dict, NcsPathway) - - # We are checking because of the various iterations of the attributes - # in the NcsPathway class where some of these attributes might - # be missing. - if CARBON_PATHS_ATTRIBUTE in source_dict: - ncs.carbon_paths = source_dict[CARBON_PATHS_ATTRIBUTE] - - return ncs - - -def create_activity(source_dict) -> typing.Union[Activity, None]: - """Factory method for creating an activity using - attribute values defined in a dictionary. - - :param source_dict: Dictionary containing property values. - :type source_dict: dict - - :returns: activity with property values set - from the dictionary. - :rtype: Activity - """ - activity = create_layer_component(source_dict, Activity) - if PRIORITY_LAYERS_SEGMENT in source_dict.keys(): - activity.priority_layers = source_dict[PRIORITY_LAYERS_SEGMENT] - - # Set style - if STYLE_ATTRIBUTE in source_dict.keys(): - activity.layer_styles = source_dict[STYLE_ATTRIBUTE] - - # Set styling pixel value - if PIXEL_VALUE_ATTRIBUTE in source_dict.keys(): - activity.style_pixel_value = source_dict[PIXEL_VALUE_ATTRIBUTE] - - return activity - - -def layer_component_to_dict( - layer_component: LayerModelComponentType, uuid_to_str=True -) -> dict: - """Creates a dictionary containing attribute - name-value pairs from a layer model component object. - - :param layer_component: Source layer model component object whose - values are to be mapped to the corresponding - attribute names. - :type layer_component: LayerModelComponent - - :param uuid_to_str: Set True to convert the UUID to a - string equivalent, else False. Some serialization engines - such as JSON are unable to handle UUID objects hence the need - to convert to string. - :type uuid_to_str: bool - - :returns: Returns a dictionary item containing attribute - name-value pairs. - :rtype: dict - """ - base_attrs = model_component_to_dict(layer_component, uuid_to_str) - base_attrs[PATH_ATTRIBUTE] = layer_component.path - base_attrs[LAYER_TYPE_ATTRIBUTE] = int(layer_component.layer_type) - base_attrs[USER_DEFINED_ATTRIBUTE] = layer_component.user_defined - - return base_attrs - - -def ncs_pathway_to_dict(ncs_pathway: NcsPathway, uuid_to_str=True) -> dict: - """Creates a dictionary containing attribute - name-value pairs from an NCS pathway object. - - This function has been retained for legacy support. - - :param ncs_pathway: Source NCS pathway object whose - values are to be mapped to the corresponding - attribute names. - :type ncs_pathway: NcsPathway - - :param uuid_to_str: Set True to convert the UUID to a - string equivalent, else False. Some serialization engines - such as JSON are unable to handle UUID objects hence the need - to convert to string. - :type uuid_to_str: bool - - :returns: Returns a dictionary item containing attribute - name-value pairs. - :rtype: dict - """ - base_ncs_dict = layer_component_to_dict(ncs_pathway, uuid_to_str) - base_ncs_dict[CARBON_PATHS_ATTRIBUTE] = ncs_pathway.carbon_paths - - return base_ncs_dict - - -def clone_layer_component( - layer_component: LayerModelComponent, - model_cls: typing.Callable[[uuid.UUID, str, str], LayerModelComponentType], -) -> typing.Union[LayerModelComponent, None]: - """Clones a layer-based model component. - - :param layer_component: Layer-based model component to clone. - :type layer_component: LayerModelComponent - - :param model_cls: Callable class that will be created based on the - input argument values from the dictionary. - :type model_cls: LayerModelComponent - - :returns: A new instance of the cloned model component. It - will return None if the input is not a layer-based model - component. - :rtype: LayerModelComponent - """ - if not isinstance(layer_component, LayerModelComponent): - return None - - cloned_component = model_cls( - layer_component.uuid, layer_component.name, layer_component.description - ) - - for f in fields(layer_component): - attr_val = getattr(layer_component, f.name) - setattr(cloned_component, f.name, attr_val) - - return cloned_component - - -def clone_ncs_pathway(ncs: NcsPathway) -> NcsPathway: - """Creates a deep copy of the given NCS pathway. - - :param ncs: NCS pathway to clone. - :type ncs: NcsPathway - - :returns: A deep copy of the original NCS pathway object. - :rtype: NcsPathway - """ - return clone_layer_component(ncs, NcsPathway) - - -def clone_activity( - activity: Activity, -) -> Activity: - """Creates a deep copy of the given activity. - - :param activity: activity to clone. - :type activity: Activity - - :returns: A deep copy of the original activity object. - :rtype: Activity - """ - activity = clone_layer_component(activity, Activity) - if activity is None: - return None - - pathways = activity.pathways - cloned_pathways = [] - for p in pathways: - cloned_ncs = clone_ncs_pathway(p) - if cloned_ncs is not None: - cloned_pathways.append(cloned_ncs) - - activity.pathways = cloned_pathways - - return activity - - -def copy_layer_component_attributes( - target: LayerModelComponent, source: LayerModelComponent -) -> LayerModelComponent: - """Copies the attribute values of source to target. The uuid - attribute value is not copied as well as the layer attribute. - However, for the latter, the path is copied. - - :param target: Target object whose attribute values will be updated. - :type target: LayerModelComponent - - :param source: Source object whose attribute values will be copied to - the target. - :type source: LayerModelComponent - - :returns: Target object containing the updated attribute values apart - for the uuid whose value will not change. - :rtype: LayerModelComponent - """ - if not isinstance(target, LayerModelComponent) or not isinstance( - source, LayerModelComponent - ): - raise TypeError( - "Source or target objects are not of type 'LayerModelComponent'" - ) - - for f in fields(source): - # Exclude uuid - if f.name == UUID_ATTRIBUTE: - continue - attr_val = getattr(source, f.name) - setattr(target, f.name, attr_val) - - # Force layer to be set/updated - target.update_layer_type() - - return target - - -def extent_to_qgs_rectangle( - spatial_extent: SpatialExtent, -) -> typing.Union[QgsRectangle, None]: - """Returns a QgsRectangle object from the SpatialExtent object. - - If the SpatialExtent is invalid (i.e. less than four items) then it - will return None. - - :param spatial_extent: Spatial extent data model that defines the - scenario bounds. - :type spatial_extent: SpatialExtent - - :returns: QGIS rectangle defining the bounds for the scenario. - :rtype: QgsRectangle - """ - if len(spatial_extent.bbox) < 4: - return None - - return QgsRectangle( - spatial_extent.bbox[0], - spatial_extent.bbox[2], - spatial_extent.bbox[1], - spatial_extent.bbox[3], - ) - - -def extent_to_project_crs_extent( - spatial_extent: SpatialExtent, project: QgsProject = None -) -> typing.Union[QgsRectangle, None]: - """Transforms SpatialExtent model to an QGIS extent based - on the CRS of the given project. - - :param spatial_extent: Spatial extent data model that defines the - scenario bounds. - :type spatial_extent: SpatialExtent - - :param project: Project whose CRS will be used to determine - the values of the output extent. - :type project: QgsProject - - :returns: Output extent in the project's CRS. If the input extent - is invalid, this function will return None. - :rtype: QgsRectangle - """ - input_rect = extent_to_qgs_rectangle(spatial_extent) - if input_rect is None: - return None - - default_crs = QgsCoordinateReferenceSystem.fromEpsgId(DEFAULT_CRS_ID) - if not default_crs.isValid(): - return None - - if project is None: - project = QgsProject.instance() - - target_crs = project.crs() - if default_crs == target_crs: - # No need for transformation - return input_rect - - try: - coordinate_xform = QgsCoordinateTransform(default_crs, project.crs(), project) - return coordinate_xform.transformBoundingBox(input_rect) - except Exception as e: - log(f"{e}, using the default input extent.") - - return input_rect - - -def activity_npv_to_dict(activity_npv: ActivityNpv) -> dict: - """Converts an ActivityNpv object to a dictionary representation. - - :returns: A dictionary containing attribute name-value pairs. - :rtype: dict - """ - return { - YEARS_ATTRIBUTE: activity_npv.params.years, - DISCOUNT_ATTRIBUTE: activity_npv.params.discount, - ABSOLUTE_NPV_ATTRIBUTE: activity_npv.params.absolute_npv, - NORMALIZED_NPV_ATTRIBUTE: activity_npv.params.normalized_npv, - YEARLY_RATES_ATTRIBUTE: activity_npv.params.yearly_rates, - ENABLED_ATTRIBUTE: activity_npv.enabled, - ACTIVITY_IDENTIFIER_PROPERTY: activity_npv.activity_id, - } - - -def create_activity_npv(activity_npv_dict: dict) -> typing.Optional[ActivityNpv]: - """Creates an ActivityNpv object from the equivalent dictionary - representation. - - Please note that the `activity` attribute of the `ActivityNpv` object will be - `None` hence, will have to be set manually by extracting the corresponding `Activity` - from the activity UUID. - - :param activity_npv_dict: Dictionary containing information for deserializing - to the ActivityNpv object. - :type activity_npv_dict: dict - - :returns: ActivityNpv deserialized from the dictionary representation. - :rtype: ActivityNpv - """ - args = [] - if YEARS_ATTRIBUTE in activity_npv_dict: - args.append(activity_npv_dict[YEARS_ATTRIBUTE]) - - if DISCOUNT_ATTRIBUTE in activity_npv_dict: - args.append(activity_npv_dict[DISCOUNT_ATTRIBUTE]) - - if ABSOLUTE_NPV_ATTRIBUTE in activity_npv_dict: - args.append(activity_npv_dict[ABSOLUTE_NPV_ATTRIBUTE]) - - if NORMALIZED_NPV_ATTRIBUTE in activity_npv_dict: - args.append(activity_npv_dict[NORMALIZED_NPV_ATTRIBUTE]) - - if len(args) < 4: - return None - - yearly_rates = [] - if YEARLY_RATES_ATTRIBUTE in activity_npv_dict: - yearly_rates = activity_npv_dict[YEARLY_RATES_ATTRIBUTE] - - npv_params = NpvParameters(*args) - npv_params.yearly_rates = yearly_rates - - npv_enabled = False - if ENABLED_ATTRIBUTE in activity_npv_dict: - npv_enabled = activity_npv_dict[ENABLED_ATTRIBUTE] - - return ActivityNpv(npv_params, npv_enabled, None) - - -def activity_npv_collection_to_dict(activity_collection: ActivityNpvCollection) -> dict: - """Converts the activity NPV collection object to the - dictionary representation. - - :returns: A dictionary containing the attribute name-value pairs - of an activity NPV collection object - :rtype: dict - """ - npv_collection_dict = { - MIN_VALUE_ATTRIBUTE: activity_collection.minimum_value, - MAX_VALUE_ATTRIBUTE: activity_collection.maximum_value, - COMPUTED_ATTRIBUTE: activity_collection.use_computed, - REMOVE_EXISTING_ATTRIBUTE: activity_collection.remove_existing, - } - - mapping_dict = list(map(activity_npv_to_dict, activity_collection.mappings)) - npv_collection_dict[NPV_MAPPINGS_ATTRIBUTE] = mapping_dict - - return npv_collection_dict - - -def create_activity_npv_collection( - activity_collection_dict: dict, reference_activities: typing.List[Activity] = None -) -> typing.Optional[ActivityNpvCollection]: - """Creates an activity NPV collection object from the corresponding - dictionary representation. - - :param activity_collection_dict: Dictionary representation containing - information of an activity NPV collection object. - :type activity_collection_dict: dict - - :param reference_activities: Optional list of activities that will be - used to lookup when deserializing the ActivityNpv objects. - :type reference_activities: list - - :returns: Activity NPV collection object from the dictionary representation - or None if the source dictionary is invalid. - :rtype: ActivityNpvCollection - """ - if len(activity_collection_dict) == 0: - return None - - ref_activities_by_uuid = { - str(activity.uuid): activity for activity in reference_activities - } - - args = [] - - # Minimum value - if MIN_VALUE_ATTRIBUTE in activity_collection_dict: - args.append(activity_collection_dict[MIN_VALUE_ATTRIBUTE]) - - # Maximum value - if MAX_VALUE_ATTRIBUTE in activity_collection_dict: - args.append(activity_collection_dict[MAX_VALUE_ATTRIBUTE]) - - if len(args) < 2: - return None - - activity_npv_collection = ActivityNpvCollection(*args) - - # Use computed - if COMPUTED_ATTRIBUTE in activity_collection_dict: - use_computed = activity_collection_dict[COMPUTED_ATTRIBUTE] - activity_npv_collection.use_computed = use_computed - - # Remove existing - if REMOVE_EXISTING_ATTRIBUTE in activity_collection_dict: - remove_existing = activity_collection_dict[REMOVE_EXISTING_ATTRIBUTE] - activity_npv_collection.remove_existing = remove_existing - - if NPV_MAPPINGS_ATTRIBUTE in activity_collection_dict: - mappings_dict = activity_collection_dict[NPV_MAPPINGS_ATTRIBUTE] - npv_mappings = [] - for md in mappings_dict: - activity_npv = create_activity_npv(md) - if activity_npv is None: - continue - - # Get the corresponding activity from the unique identifier - if ACTIVITY_IDENTIFIER_PROPERTY in md: - activity_id = md[ACTIVITY_IDENTIFIER_PROPERTY] - if activity_id in ref_activities_by_uuid: - ref_activity = ref_activities_by_uuid[activity_id] - activity_npv.activity = ref_activity - npv_mappings.append(activity_npv) - - activity_npv_collection.mappings = npv_mappings - - return activity_npv_collection diff --git a/django_project/cplus/models/report.py b/django_project/cplus/models/report.py deleted file mode 100644 index fac5808..0000000 --- a/django_project/cplus/models/report.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- - -""" Data models for report production.""" - -import dataclasses -import typing -from uuid import UUID - -from qgis.core import QgsFeedback, QgsRectangle - -from .base import Scenario - - -@dataclasses.dataclass -class ReportContext: - """Context information for generating a report.""" - - template_path: str - scenario: Scenario - name: str - scenario_output_dir: str - project_file: str - feedback: QgsFeedback - output_layer_name: str - - -@dataclasses.dataclass -class ReportSubmitStatus: - """Result of report submission process.""" - - status: bool - feedback: QgsFeedback - - -@dataclasses.dataclass -class ReportResult: - """Detailed result information from a report generation - run. - """ - - success: bool - scenario_id: UUID - output_dir: str - # Error messages - messages: typing.Tuple[str] = dataclasses.field(default_factory=tuple) - # Layout name - name: str = "" - - @property - def pdf_path(self) -> str: - """Returns the absolute path to the PDF file if the process - completed successfully. - - Caller needs to verify if the file actually exists in the - given location. - - :returns: Absolute path to the PDF file if the process - completed successfully else an empty string. - :rtype: str - """ - if not self.output_dir or not self.name: - return "" - - return f"{self.output_dir}/{self.name}.pdf" diff --git a/django_project/cplus/tasks/__init__.py b/django_project/cplus/tasks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_project/cplus/tasks/analysis.py b/django_project/cplus/tasks/analysis.py deleted file mode 100644 index 2a557de..0000000 --- a/django_project/cplus/tasks/analysis.py +++ /dev/null @@ -1,2151 +0,0 @@ -# coding=utf-8 -""" - Plugin tasks related to the scenario analysis - -""" -import datetime -import os -import uuid -from pathlib import Path - -import math -from qgis import processing -from qgis.PyQt import QtCore -from qgis.core import ( - Qgis, - QgsCoordinateReferenceSystem, - QgsProcessing, - QgsProcessingContext, - QgsProcessingFeedback, - QgsRasterLayer, - QgsRectangle, - QgsVectorLayer, - QgsWkbTypes, - QgsTask -) - -from cplus.utils.conf import settings_manager, Settings -from cplus.definitions.defaults import ( - SCENARIO_OUTPUT_FILE_NAME, -) -from cplus.models.base import ScenarioResult -from cplus.models.helpers import clone_activity -from cplus.utils.helper import ( - align_rasters, - clean_filename, - tr, - log, - FileUtils -) - - -class ScenarioAnalysisTask(QgsTask): - """Prepares and runs the scenario analysis""" - - status_message_changed = QtCore.pyqtSignal(str) - info_message_changed = QtCore.pyqtSignal(str, int) - - custom_progress_changed = QtCore.pyqtSignal(float) - - def __init__( - self, - analysis_scenario_name, - analysis_scenario_description, - analysis_activities, - analysis_priority_layers_groups, - analysis_extent, - scenario, - ): - super().__init__() - self.analysis_scenario_name = analysis_scenario_name - self.analysis_scenario_description = analysis_scenario_description - - self.analysis_activities = analysis_activities - self.analysis_priority_layers_groups = analysis_priority_layers_groups - self.analysis_extent = analysis_extent - self.analysis_extent_string = None - - self.analysis_weighted_activities = [] - self.scenario_result = None - self.scenario_directory = None - - self.success = True - self.output = None - self.error = None - self.status_message = None - - self.info_message = None - - self.processing_cancelled = False - self.feedback = QgsProcessingFeedback() - self.processing_context = QgsProcessingContext() - - self.scenario = scenario - - def get_settings_value(self, name: str, default=None, setting_type=None): - return settings_manager.get_value(name, default, setting_type) - - def get_scenario_directory(self): - base_dir = self.get_settings_value(Settings.BASE_DIR) - return os.path.join( - f"{base_dir}", - "scenario_" f'{datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")}', - ) - - def get_priority_layer(self, identifier): - return settings_manager.get_priority_layer(identifier) - - def get_activity(self, activity_uuid): - return settings_manager.get_activity(activity_uuid) - - def get_priority_layers(self): - return settings_manager.get_priority_layers() - - def get_masking_layers(self): - masking_layers_paths = self.get_settings_value( - Settings.MASK_LAYERS_PATHS, default=None - ) - masking_layers = masking_layers_paths.split(",") if masking_layers_paths else [] - - masking_layers.remove("") if "" in masking_layers else None - return masking_layers - - def cancel_task(self, exception=None): - self.error = exception - self.cancel() - - def log_message( - self, - message: str, - name: str = "qgis_cplus", - info: bool = True, - notify: bool = True, - ): - log(message, name=name, info=info, notify=notify) - - def on_terminated(self): - """Called when the task is terminated.""" - message = "Processing has been cancelled by the user." - if self.error: - message = f"Problem in running scenario analysis: {self.error}" - self.set_status_message(tr(message)) - self.log_message(message) - - def run(self): - """Runs the main scenario analysis task operations""" - - self.scenario_directory = self.get_scenario_directory() - - FileUtils.create_new_dir(self.scenario_directory) - - selected_pathway = None - pathway_found = False - - for activity in self.analysis_activities: - if pathway_found: - break - for pathway in activity.pathways: - if pathway is not None: - pathway_found = True - selected_pathway = pathway - break - - target_layer = QgsRasterLayer(selected_pathway.path, selected_pathway.name) - - dest_crs = ( - target_layer.crs() - if selected_pathway and selected_pathway.path - else QgsCoordinateReferenceSystem("EPSG:4326") - ) - - processing_extent = QgsRectangle( - float(self.analysis_extent.bbox[0]), - float(self.analysis_extent.bbox[2]), - float(self.analysis_extent.bbox[1]), - float(self.analysis_extent.bbox[3]), - ) - - snapped_extent = self.align_extent(target_layer, processing_extent) - - extent_string = ( - f"{snapped_extent.xMinimum()},{snapped_extent.xMaximum()}," - f"{snapped_extent.yMinimum()},{snapped_extent.yMaximum()}" - f" [{dest_crs.authid()}]" - ) - - self.log_message( - "Original area of interest extent: " - f"{processing_extent.asWktPolygon()} \n" - ) - self.log_message( - "Snapped area of interest extent " f"{snapped_extent.asWktPolygon()} \n" - ) - # Run pathways layers snapping using a specified reference layer - - snapping_enabled = self.get_settings_value( - Settings.SNAPPING_ENABLED, default=False, setting_type=bool - ) - reference_layer = self.get_settings_value(Settings.SNAP_LAYER, default="") - reference_layer_path = Path(reference_layer) - if ( - snapping_enabled - and os.path.exists(reference_layer) - and reference_layer_path.is_file() - ): - self.snap_analysis_data( - self.analysis_activities, - extent_string, - ) - - # Preparing all the pathways by adding them together with - # their carbon layers before creating - # their respective activities. - - save_output = self.get_settings_value( - Settings.NCS_WITH_CARBON, default=True, setting_type=bool - ) - - self.run_pathways_analysis( - self.analysis_activities, - extent_string, - temporary_output=not save_output, - ) - - # Normalizing all the activities pathways using the carbon coefficient and - # the pathway suitability index - - self.run_pathways_normalization( - self.analysis_activities, - extent_string, - ) - - # Creating activities from the normalized pathways - - save_output = self.get_settings_value( - Settings.LANDUSE_PROJECT, default=True, setting_type=bool - ) - - self.run_activities_analysis( - self.analysis_activities, - extent_string, - temporary_output=not save_output, - ) - - # Run masking of the activities layers - masking_layers = self.get_masking_layers() - - if masking_layers: - self.run_activities_masking( - self.analysis_activities, - masking_layers, - extent_string, - ) - - sieve_enabled = self.get_settings_value( - Settings.SIEVE_ENABLED, default=False, setting_type=bool - ) - - if sieve_enabled: - self.run_activities_sieve( - self.analysis_activities, - ) - - # After creating activities, we normalize them using the same coefficients - # used in normalizing their respective pathways. - - save_output = self.get_settings_value( - Settings.LANDUSE_NORMALIZED, default=True, setting_type=bool - ) - - self.run_activities_normalization( - self.analysis_activities, - extent_string, - temporary_output=not save_output, - ) - - # Weighting the activities with their corresponding priority weighting layers - save_output = self.get_settings_value( - Settings.LANDUSE_WEIGHTED, default=True, setting_type=bool - ) - weighted_activities, result = self.run_activities_weighting( - self.analysis_activities, - self.analysis_priority_layers_groups, - extent_string, - temporary_output=not save_output, - ) - - self.analysis_weighted_activities = weighted_activities - self.scenario.weighted_activities = weighted_activities - - # Post weighting analysis - self.run_activities_cleaning( - weighted_activities, extent_string, temporary_output=not save_output - ) - - # The highest position tool analysis - save_output = self.get_settings_value( - Settings.HIGHEST_POSITION, default=True, setting_type=bool - ) - self.run_highest_position_analysis(temporary_output=not save_output) - - return True - - def finished(self, result: bool): - """Calls the handler responsible for doing post analysis workflow. - - :param result: Whether the run() operation finished successfully - :type result: bool - """ - if result: - self.log_message("Finished from the main task \n") - else: - self.log_message(f"Error from task scenario task {self.error}") - - def set_status_message(self, message): - self.status_message = message - self.status_message_changed.emit(self.status_message) - - def set_info_message(self, message, level=Qgis.Info): - self.info_message = message - self.info_message_changed.emit(self.info_message, level) - - def set_custom_progress(self, value): - self.custom_progress = value - self.custom_progress_changed.emit(self.custom_progress) - - def update_progress(self, value): - """Sets the value of the task progress - - :param value: Value to be set on the progress bar - :type value: float - """ - if not self.processing_cancelled: - self.set_custom_progress(value) - else: - self.feedback = QgsProcessingFeedback() - self.processing_context = QgsProcessingContext() - - def align_extent(self, raster_layer, target_extent): - """Snaps the passed extent to the activities pathway layer pixel bounds - - :param raster_layer: The target layer that the passed extent will be - aligned with - :type raster_layer: QgsRasterLayer - - :param target_extent: Spatial extent that will be used a target extent when - doing alignment. - :type target_extent: QgsRectangle - """ - - try: - raster_extent = raster_layer.extent() - - x_res = raster_layer.rasterUnitsPerPixelX() - y_res = raster_layer.rasterUnitsPerPixelY() - - left = raster_extent.xMinimum() + x_res * math.floor( - (target_extent.xMinimum() - raster_extent.xMinimum()) / x_res - ) - right = raster_extent.xMinimum() + x_res * math.ceil( - (target_extent.xMaximum() - raster_extent.xMinimum()) / x_res - ) - bottom = raster_extent.yMinimum() + y_res * math.floor( - (target_extent.yMinimum() - raster_extent.yMinimum()) / y_res - ) - top = raster_extent.yMaximum() - y_res * math.floor( - (raster_extent.yMaximum() - target_extent.yMaximum()) / y_res - ) - - return QgsRectangle(left, bottom, right, top) - - except Exception as e: - self.log_message( - tr( - f"Problem snapping area of " - f"interest extent, using the original extent," - f"{str(e)}" - ) - ) - - return target_extent - - def replace_nodata(self, layer_path, output_path, nodata_value): - """Adds nodata value info into the layer available - in the passed layer_path and save the layer in the passed output_path - path. - - The addition will replace any current nodata value available in - the input layer. - - :param layer_path: Input layer path - :type layer_path: str - - :param output_path: Output layer path - :type output_path: str - - :param nodata_value: Nodata value to be used - :type output_path: int - - :returns: Whether the task operations was successful - :rtype: bool - - """ - self.feedback = QgsProcessingFeedback() - self.feedback.progressChanged.connect(self.update_progress) - - try: - alg_params = { - "COPY_SUBDATASETS": False, - "DATA_TYPE": 6, # Float32 - "EXTRA": "", - "INPUT": layer_path, - "NODATA": None, - "OPTIONS": "", - "TARGET_CRS": None, - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - translate_output = processing.run( - "gdal:translate", - alg_params, - context=self.processing_context, - feedback=self.feedback, - is_child_algorithm=True, - ) - - alg_params = { - "DATA_TYPE": 0, # Use Input Layer Data Type - "EXTRA": "", - "INPUT": translate_output["OUTPUT"], - "MULTITHREADING": False, - "NODATA": -9999, - "OPTIONS": "", - "RESAMPLING": 0, # Nearest Neighbour - "SOURCE_CRS": None, - "TARGET_CRS": None, - "TARGET_EXTENT": None, - "TARGET_EXTENT_CRS": None, - "TARGET_RESOLUTION": None, - "OUTPUT": output_path, - } - outputs = processing.run( - "gdal:warpreproject", - alg_params, - context=self.processing_context, - feedback=self.feedback, - is_child_algorithm=True, - ) - - return outputs is not None - except Exception as e: - log(f"Problem replacing no data value from a snapping output, {e}") - - return False - - def run_pathways_analysis(self, activities, extent, temporary_output=False): - """Runs the required activity pathways analysis on the passed - activities. The analysis involves adding the pathways - carbon layers into their respective pathway layers. - - If a pathway layer has more than one carbon layer, the resulting - weighted pathway will contain the sum of the pathway layer values - with the average of the pathway carbon layers values. - - :param activities: List of the selected activities - :type activities: typing.List[Activity] - - :param extent: The selected extent from user - :type extent: SpatialExtent - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - return False - - self.set_status_message(tr("Adding activity pathways with carbon layers")) - - pathways = [] - activities_paths = [] - - try: - for activity in activities: - if not activity.pathways and ( - activity.path is None or activity.path == "" - ): - self.set_info_message( - tr( - f"No defined activity pathways or an" - f" activity layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"No defined activity pathways or a " - f"activity layer for the activity {activity.name}" - ) - return False - - for pathway in activity.pathways: - if not (pathway in pathways): - pathways.append(pathway) - - if activity.path is not None and activity.path != "": - activities_paths.append(activity.path) - - if not pathways and len(activities_paths) > 0: - self.run_pathways_normalization(activities, extent) - return - - suitability_index = float( - self.get_settings_value(Settings.PATHWAY_SUITABILITY_INDEX, default=0) - ) - - carbon_coefficient = float( - self.get_settings_value(Settings.CARBON_COEFFICIENT, default=0.0) - ) - - for pathway in pathways: - basenames = [] - layers = [] - path_basename = Path(pathway.path).stem - layers.append(pathway.path) - - file_name = clean_filename(pathway.name.replace(" ", "_")) - - if suitability_index > 0: - basenames.append(f'{suitability_index} * "{path_basename}@1"') - else: - basenames.append(f'"{path_basename}@1"') - - carbon_names = [] - - if len(pathway.carbon_paths) <= 0: - continue - - new_carbon_directory = os.path.join( - self.scenario_directory, "pathways_carbon_layers" - ) - - FileUtils.create_new_dir(new_carbon_directory) - - output_file = os.path.join( - new_carbon_directory, f"{file_name}_{str(uuid.uuid4())[:4]}.tif" - ) - - for carbon_path in pathway.carbon_paths: - carbon_full_path = Path(carbon_path) - if not carbon_full_path.exists(): - continue - layers.append(carbon_path) - carbon_names.append(f'"{carbon_full_path.stem}@1"') - - if len(carbon_names) == 1 and carbon_coefficient > 0: - basenames.append(f"{carbon_coefficient} * ({carbon_names[0]})") - - # Setting up calculation to use carbon layers average when - # a pathway has more than one carbon layer. - if len(carbon_names) > 1 and carbon_coefficient > 0: - basenames.append( - f"{carbon_coefficient} * (" - f'({" + ".join(carbon_names)}) / ' - f"{len(pathway.carbon_paths)})" - ) - expression = " + ".join(basenames) - - if carbon_coefficient <= 0 and suitability_index <= 0: - self.run_pathways_normalization(activities, extent) - return - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - alg_params = { - "CELLSIZE": 0, - "CRS": None, - "EXPRESSION": expression, - "EXTENT": extent, - "LAYERS": layers, - "OUTPUT": output, - } - - self.log_message( - f"Used parameters for combining pathways" - f" and carbon layers generation: {alg_params} \n" - ) - - self.feedback = QgsProcessingFeedback() - - self.feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "qgis:rastercalculator", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - pathway.path = results["OUTPUT"] - except Exception as e: - self.log_message(f"Problem running pathway analysis, {e}") - self.cancel_task(e) - - return True - - def snap_analysis_data(self, activities, extent): - """Snaps the passed activities pathways, carbon layers and priority layers - to align with the reference layer set on the settings - manager. - - :param activities: List of the selected activities - :type activities: typing.List[Activity] - - :param extent: The selected extent from user - :type extent: list - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message( - tr( - "Snapping the selected activity pathways, " - "carbon layers and priority layers" - ) - ) - - pathways = [] - - try: - for activity in activities: - if not activity.pathways and ( - activity.path is None or activity.path == "" - ): - self.set_info_message( - tr( - f"No defined activity pathways or a" - f" activity layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"No defined activity pathways or a " - f"activity layer for the activity {activity.name}" - ) - return False - - for pathway in activity.pathways: - if not (pathway in pathways): - pathways.append(pathway) - - reference_layer_path = self.get_settings_value(Settings.SNAP_LAYER) - rescale_values = self.get_settings_value( - Settings.RESCALE_VALUES, default=False, setting_type=bool - ) - - resampling_method = self.get_settings_value( - Settings.RESAMPLING_METHOD, default=0 - ) - - if pathways is not None and len(pathways) > 0: - snapped_pathways_directory = os.path.join( - self.scenario_directory, "pathways" - ) - - FileUtils.create_new_dir(snapped_pathways_directory) - - for pathway in pathways: - pathway_layer = QgsRasterLayer(pathway.path, pathway.name) - nodata_value = pathway_layer.dataProvider().sourceNoDataValue(1) - - if self.processing_cancelled: - return False - - # carbon layer snapping - - self.log_message( - f"Snapping carbon layers from {pathway.name} pathway" - ) - - if ( - pathway.carbon_paths is not None - and len(pathway.carbon_paths) > 0 - ): - snapped_carbon_directory = os.path.join( - self.scenario_directory, "carbon_layers" - ) - - FileUtils.create_new_dir(snapped_carbon_directory) - - snapped_carbon_paths = [] - - for carbon_path in pathway.carbon_paths: - carbon_layer = QgsRasterLayer( - carbon_path, f"{str(uuid.uuid4())[:4]}" - ) - nodata_value_carbon = ( - carbon_layer.dataProvider().sourceNoDataValue(1) - ) - - carbon_output_path = self.snap_layer( - carbon_path, - reference_layer_path, - extent, - snapped_carbon_directory, - rescale_values, - resampling_method, - nodata_value_carbon, - ) - - if carbon_output_path: - snapped_carbon_paths.append(carbon_output_path) - else: - snapped_carbon_paths.append(carbon_path) - - pathway.carbon_paths = snapped_carbon_paths - - self.log_message(f"Snapping {pathway.name} pathway layer \n") - - # Pathway snapping - - output_path = self.snap_layer( - pathway.path, - reference_layer_path, - extent, - snapped_pathways_directory, - rescale_values, - resampling_method, - nodata_value, - ) - if output_path: - pathway.path = output_path - - for activity in activities: - self.log_message( - f"Snapping {len(activity.priority_layers)} " - f"priority weighting layers from activity {activity.name} with layers\n" - ) - - if ( - activity.priority_layers is not None - and len(activity.priority_layers) > 0 - ): - snapped_priority_directory = os.path.join( - self.scenario_directory, "priority_layers" - ) - - FileUtils.create_new_dir(snapped_priority_directory) - - priority_layers = [] - for priority_layer in activity.priority_layers: - if priority_layer is None: - continue - - priority_layer_settings = self.get_priority_layer( - priority_layer.get("uuid") - ) - if priority_layer_settings is None: - continue - - priority_layer_path = priority_layer_settings.get("path") - - if not Path(priority_layer_path).exists(): - priority_layers.append(priority_layer) - continue - - layer = QgsRasterLayer( - priority_layer_path, f"{str(uuid.uuid4())[:4]}" - ) - nodata_value_priority = layer.dataProvider().sourceNoDataValue( - 1 - ) - - priority_output_path = self.snap_layer( - priority_layer_path, - reference_layer_path, - extent, - snapped_priority_directory, - rescale_values, - resampling_method, - nodata_value_priority, - ) - - if priority_output_path: - priority_layer["path"] = priority_output_path - - priority_layers.append(priority_layer) - - activity.priority_layers = priority_layers - - except Exception as e: - self.log_message(f"Problem snapping layers, {e} \n") - self.cancel_task(e) - return False - - return True - - def snap_layer( - self, - input_path, - reference_path, - extent, - directory, - rescale_values, - resampling_method, - nodata_value, - ): - """Snaps the passed input layer using the reference layer and updates - the snap output no data value to be the same as the original input layer - no data value. - - :param input_path: Input layer source - :type input_path: str - - :param reference_path: Reference layer source - :type reference_path: str - - :param extent: Clip extent - :type extent: list - - :param directory: Absolute path of the output directory for the snapped - layers - :type directory: str - - :param rescale_values: Whether to rescale pixel values - :type rescale_values: bool - - :param resample_method: Method to use when resampling - :type resample_method: QgsAlignRaster.ResampleAlg - - :param nodata_value: Original no data value of the input layer - :type nodata_value: float - - """ - - input_result_path, reference_result_path = align_rasters( - input_path, - reference_path, - extent, - directory, - rescale_values, - resampling_method, - ) - - if input_result_path is not None: - result_path = Path(input_result_path) - - directory = result_path.parent - name = result_path.stem - - output_path = os.path.join(directory, f"{name}_final.tif") - - self.replace_nodata(input_result_path, output_path, nodata_value) - - return output_path - - def run_pathways_normalization(self, activities, extent, temporary_output=False): - """Runs the normalization on the activities pathways layers, - adjusting band values measured on different scale, the resulting scale - is computed using the below formula - Normalized_Pathway = (Carbon coefficient + Suitability index) * ( - (activity layer value) - (activity band minimum value)) / - (activity band maximum value - activity band minimum value)) - - If the carbon coefficient and suitability index are both zero then - the computation won't take them into account in the normalization - calculation. - - :param activities: List of the analyzed activities - :type activities: typing.List[Activity] - - :param extent: selected extent from user - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message(tr("Normalization of pathways")) - - pathways = [] - activities_paths = [] - - try: - for activity in activities: - if not activity.pathways and ( - activity.path is None or activity.path == "" - ): - self.set_info_message( - tr( - f"No defined activity pathways or an" - f" activity layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"No defined activity pathways or an " - f"activity layer for the activity {activity.name}" - ) - - return False - - for pathway in activity.pathways: - if not (pathway in pathways): - pathways.append(pathway) - - if activity.path is not None and activity.path != "": - activities_paths.append(activity.path) - - if not pathways and len(activities_paths) > 0: - self.run_activities_analysis(activities, extent) - - return - - carbon_coefficient = float( - self.get_settings_value(Settings.CARBON_COEFFICIENT, default=0.0) - ) - - suitability_index = float( - self.get_settings_value(Settings.PATHWAY_SUITABILITY_INDEX, default=0) - ) - - normalization_index = carbon_coefficient + suitability_index - - for pathway in pathways: - layers = [] - normalized_pathways_directory = os.path.join( - self.scenario_directory, "normalized_pathways" - ) - FileUtils.create_new_dir(normalized_pathways_directory) - file_name = clean_filename(pathway.name.replace(" ", "_")) - - output_file = os.path.join( - normalized_pathways_directory, - f"{file_name}_{str(uuid.uuid4())[:4]}.tif", - ) - - pathway_layer = QgsRasterLayer(pathway.path, pathway.name) - provider = pathway_layer.dataProvider() - band_statistics = provider.bandStatistics(1) - - min_value = band_statistics.minimumValue - max_value = band_statistics.maximumValue - - layer_name = Path(pathway.path).stem - - layers.append(pathway.path) - - self.log_message( - f"Found minimum {min_value} and " - f"maximum {max_value} for pathway " - f" \n" - ) - - if max_value < min_value: - raise Exception( - tr( - f"Pathway contains " - f"invalid minimum and maximum band values" - ) - ) - - if normalization_index > 0: - expression = ( - f" {normalization_index} * " - f'("{layer_name}@1" - {min_value}) /' - f" ({max_value} - {min_value})" - ) - else: - expression = ( - f'("{layer_name}@1" - {min_value}) /' - f" ({max_value} - {min_value})" - ) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - alg_params = { - "CELLSIZE": 0, - "CRS": None, - "EXPRESSION": expression, - "EXTENT": extent, - "LAYERS": layers, - "OUTPUT": output, - } - - self.log_message( - f"Used parameters for normalization of the pathways: {alg_params} \n" - ) - - self.feedback = QgsProcessingFeedback() - - self.feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "qgis:rastercalculator", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - # self.replace_nodata(results["OUTPUT"], output_file, -9999) - - pathway.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem normalizing pathways layers, {e} \n") - self.cancel_task(e) - return False - - return True - - def run_activities_analysis(self, activities, extent, temporary_output=False): - """Runs the required activity analysis on the passed - activities pathways. The analysis is responsible for creating activities - layers from their respective pathways layers. - - :param activities: List of the selected activities - :type activities: typing.List[Activity] - - :param extent: selected extent from user - :type extent: SpatialExtent - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message(tr("Creating activity layers from pathways")) - - try: - for activity in activities: - activities_directory = os.path.join( - self.scenario_directory, "activities" - ) - FileUtils.create_new_dir(activities_directory) - file_name = clean_filename(activity.name.replace(" ", "_")) - - layers = [] - if not activity.pathways and ( - activity.path is None or activity.path == "" - ): - self.set_info_message( - tr( - f"No defined activity pathways or a" - f" activity layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"No defined activity pathways or an " - f"activity layer for the activity {activity.name}" - ) - - return False - - output_file = os.path.join( - activities_directory, f"{file_name}_{str(uuid.uuid4())[:4]}.tif" - ) - - # Due to the activities base class - # activity only one of the following blocks will be executed, - # the activity either contain a path or - # pathways - - if activity.path is not None and activity.path != "": - layers = [activity.path] - - for pathway in activity.pathways: - layers.append(pathway.path) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - - alg_params = { - "IGNORE_NODATA": True, - "INPUT": layers, - "EXTENT": extent, - "OUTPUT_NODATA_VALUE": -9999, - "REFERENCE_LAYER": layers[0] if len(layers) > 0 else None, - "STATISTIC": 0, # Sum - "OUTPUT": output, - } - - self.log_message( - f"Used parameters for " f"activities generation: {alg_params} \n" - ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "native:cellstatistics", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - activity.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem creating activity layers, {e}") - self.cancel_task(e) - return False - - return True - - def run_activities_masking( - self, activities, masking_layers, extent, temporary_output=False - ): - """Applies the mask layers into the passed activities - - :param activities: List of the selected activities - :type activities: typing.List[Activity] - - :param masking_layers: Paths to the mask layers to be used - :type masking_layers: dict - - :param extent: selected extent from user - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message(tr("Masking activities using the saved masked layers")) - - try: - if len(masking_layers) < 1: - return False - - if len(masking_layers) > 1: - initial_mask_layer = self.merge_vector_layers(masking_layers) - else: - mask_layer_path = masking_layers[0] - initial_mask_layer = QgsVectorLayer(mask_layer_path, "mask", "ogr") - - if not initial_mask_layer.isValid(): - self.log_message( - f"Skipping activities masking " - f"using layer {mask_layer_path}, not a valid layer." - ) - return False - - if Qgis.versionInt() < 33000: - layer_check = initial_mask_layer.geometryType() == QgsWkbTypes.Polygon - else: - layer_check = ( - initial_mask_layer.geometryType() == Qgis.GeometryType.Polygon - ) - - if not layer_check: - self.log_message( - f"Skipping activities masking " - f"using layer {mask_layer_path}, not a polygon layer." - ) - return False - - extent_layer = self.layer_extent(extent) - mask_layer = self.mask_layer_difference(initial_mask_layer, extent_layer) - - if isinstance(mask_layer, str): - mask_layer = QgsVectorLayer(mask_layer, "ogr") - - if not mask_layer.isValid(): - self.log_message( - f"Skipping activities masking " - f"the created difference mask layer {mask_layer.source()}," - f" not a valid layer." - ) - return False - - for activity in activities: - if activity.path is None or activity.path == "": - if not self.processing_cancelled: - self.set_info_message( - tr( - f"Problem when masking activities, " - f"there is no map layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"Problem when masking activities, " - f"there is no map layer for the activity {activity.name}" - ) - else: - # If the user cancelled the processing - self.set_info_message( - tr(f"Processing has been cancelled by the user."), - level=Qgis.Critical, - ) - self.log_message(f"Processing has been cancelled by the user.") - - return False - - masked_activities_directory = os.path.join( - self.scenario_directory, "masked_activities" - ) - FileUtils.create_new_dir(masked_activities_directory) - file_name = clean_filename(activity.name.replace(" ", "_")) - - output_file = os.path.join( - masked_activities_directory, - f"{file_name}_{str(uuid.uuid4())[:4]}.tif", - ) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - activity_layer = QgsRasterLayer(activity.path, "activity_layer") - - # Actual processing calculation - alg_params = { - "INPUT": activity.path, - "MASK": mask_layer, - "SOURCE_CRS": activity_layer.crs(), - "DESTINATION_CRS": activity_layer.crs(), - "TARGET_EXTENT": extent, - "OUTPUT": output, - "NO_DATA": -9999, - } - - self.log_message( - f"Used parameters for masking the activities: {alg_params} \n" - ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "gdal:cliprasterbymasklayer", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - activity.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem masking activities layers, {e} \n") - self.cancel_task(e) - return False - - return True - - def merge_vector_layers(self, layers): - """Merges the passed vector layers into a single layer - - :param layers: List of the vector layers paths - :type layers: typing.List[str] - - :return: Merged vector layer - :rtype: QgsMapLayer - """ - - input_map_layers = [] - - for layer_path in layers: - layer = QgsVectorLayer(layer_path, "mask", "ogr") - if layer.isValid(): - input_map_layers.append(layer) - else: - self.log_message( - f"Skipping invalid mask layer {layer_path} from masking." - ) - if len(input_map_layers) == 0: - return None - if len(input_map_layers) == 1: - return input_map_layers[0].source() - - self.set_status_message(tr("Merging mask layers")) - - # Actual processing calculation - alg_params = { - "LAYERS": input_map_layers, - "CRS": None, - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - - self.log_message(f"Used parameters for merging mask layers: {alg_params} \n") - - results = processing.run( - "native:mergevectorlayers", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - return results["OUTPUT"] - - def layer_extent(self, extent): - """Creates a new vector layer contains has a - feature with geometry matching an extent parameter. - :param extent: Extent parameter - :type extent: str - :returns: Vector layer - :rtype: QgsVectorLayer - """ - - alg_params = { - "INPUT": extent, - "CRS": None, - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - - results = processing.run( - "native:extenttolayer", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - return results["OUTPUT"] - - def mask_layer_difference(self, input_layer, overlay_layer): - """Creates a new vector layer that contains - difference of features between the two passed layers. - :param input_layer: Input layer - :type input_layer: QgsVectorLayer - :param overlay_layer: Target overlay layer - :type overlay_layer: QgsVectorLayer - :returns: Vector layer - :rtype: QgsVectorLayer - """ - - alg_params = { - "INPUT": input_layer, - "OVERLAY": overlay_layer, - "OVERLAY_FIELDS_PREFIX": "", - "GRID_SIZE": None, - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - - results = processing.run( - "native:symmetricaldifference", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - return results["OUTPUT"] - - def run_activities_sieve(self, models, temporary_output=False): - """Runs the sieve functionality analysis on the passed models layers, - removing the models layer polygons that are smaller than the provided - threshold size (in pixels) and replaces them with the pixel value of - the largest neighbour polygon. - - :param models: List of the analyzed implementation models - :type models: typing.List[ImplementationModel] - - :param extent: Selected area of interest extent - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message( - tr("Applying sieve function to the implementation models") - ) - - try: - for model in models: - if model.path is None or model.path == "": - if not self.processing_cancelled: - self.set_info_message( - tr( - f"Problem when running sieve function on models, " - f"there is no map layer for the model {model.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"Problem when running sieve function on models, " - f"there is no map layer for the model {model.name}" - ) - else: - # If the user cancelled the processing - self.set_info_message( - tr(f"Processing has been cancelled by the user."), - level=Qgis.Critical, - ) - self.log_message(f"Processing has been cancelled by the user.") - - return False - - sieved_ims_directory = os.path.join( - self.scenario_directory, "sieved_ims" - ) - FileUtils.create_new_dir(sieved_ims_directory) - file_name = clean_filename(model.name.replace(" ", "_")) - - output_file = os.path.join( - sieved_ims_directory, f"{file_name}_{str(uuid.uuid4())[:4]}.tif" - ) - - threshold_value = float( - self.get_settings_value(Settings.SIEVE_THRESHOLD, default=10.0) - ) - - mask_layer = self.get_settings_value( - Settings.SIEVE_MASK_PATH, default="" - ) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - # alg_params = { - # "INPUT": model.path, - # "THRESHOLD": threshold_value, - # "MASK_LAYER": mask_layer, - # "OUTPUT": output, - # } - - input_name = os.path.splitext(os.path.basename(model.path))[0] - - # Step 1: Create a binary mask from the original raster - binary_mask = processing.run( - "qgis:rastercalculator", - { - "CELLSIZE": 0, - "LAYERS": [model.path], - "CRS": None, - "EXPRESSION": f"{input_name}@1 > 0", - "OUTPUT": "TEMPORARY_OUTPUT", - }, - )["OUTPUT"] - - # feedback.pushInfo(f"binary mask {binary_mask}") - - # binary_mask_layer = QgsRasterLayer(binary_mask, 'binary') - - # QgsProject.instance().addMapLayer(binary_mask_layer) - - # Step 2: Run sieve analysis from on the binary mask - sieved_mask = processing.run( - "gdal:sieve", - { - "INPUT": binary_mask, - "THRESHOLD": threshold_value, - "EIGHT_CONNECTEDNESS": True, - "NO_MASK": True, - "MASK_LAYER": None, - "OUTPUT": "TEMPORARY_OUTPUT", - }, - context=self.processing_context, - feedback=self.feedback, - )["OUTPUT"] - - # feedback.pushInfo(f"sieved mask {sieved_mask}") - - # sieved_mask_layer = QgsRasterLayer(sieved_mask, 'sieved_mask') - - # QgsProject.instance().addMapLayer(sieved_mask_layer) - - expr = f"({os.path.splitext(os.path.basename(sieved_mask))[0]}@1 > 0) * {os.path.splitext(os.path.basename(sieved_mask))[0]}@1" - # feedback.pushInfo(f"used expression {expr}") - - # Step 3: Remove and convert any no data value to 0 - sieved_mask_clean = processing.run( - "qgis:rastercalculator", - { - "CELLSIZE": 0, - "LAYERS": [sieved_mask], - "CRS": None, - "EXPRESSION": expr, - "OUTPUT": "TEMPORARY_OUTPUT", - }, - context=self.processing_context, - feedback=self.feedback, - )["OUTPUT"] - - # feedback.pushInfo(f"sieved mask clean {sieved_mask_clean}") - - # sieved_mask_clean_layer = QgsRasterLayer(sieved_mask_clean, 'sieved_mask_clean') - - # QgsProject.instance().addMapLayer(sieved_mask_clean_layer) - - expr_2 = f"{input_name}@1 * {os.path.splitext(os.path.basename(sieved_mask_clean))[0]}@1" - - # feedback.pushInfo(f"Used expression 2 {expr_2}") - - # Step 4: Join the sieved mask with the original input layer to filter out the small areas - sieve_output = processing.run( - "qgis:rastercalculator", - { - "CELLSIZE": 0, - "LAYERS": [model.path, sieved_mask_clean], - "CRS": None, - "EXPRESSION": expr_2, - "OUTPUT": "TEMPORARY_OUTPUT", - }, - context=self.processing_context, - feedback=self.feedback, - )["OUTPUT"] - - # feedback.pushInfo(f"sieved output joined {sieve_output}") - - # sieve_output_layer = QgsRasterLayer(sieve_output, 'sieve_output') - - # QgsProject.instance().addMapLayer(sieve_output_layer) - - # expr_3 = f'if ( {os.path.splitext(os.path.basename(sieve_output))[0]}@1 <= 0, -9999, {os.path.splitext(os.path.basename(sieve_output))[0]}@1 )' - - # feedback.pushInfo(f"used expression 3 {expr_3}") - - # Step 5. Replace all 0 with -9999 using if ("combined@1" <= 0, -9999, "combined@1") - sieve_output_updated = processing.run( - "gdal:rastercalculator", - { - "INPUT_A": f"{sieve_output}", - "BAND_A": 1, - "FORMULA": "9999*(A<=0)*(-1)+A*(A>0)", - "NO_DATA": None, - "EXTENT_OPT": 0, - "PROJWIN": None, - "RTYPE": 5, - "OPTIONS": "", - "EXTRA": "", - "OUTPUT": "TEMPORARY_OUTPUT", - }, - context=self.processing_context, - feedback=self.feedback, - )["OUTPUT"] - - # feedback.pushInfo(f"sieved output updated {sieve_output_updated}") - - # sieve_output_updated_layer = QgsRasterLayer(sieve_output_updated, 'sieve_output_updated') - - # QgsProject.instance().addMapLayer(sieve_output_updated_layer) - - # Step 6. Run sum statistics with ignore no data values set to false and no data value of -9999 - results = processing.run( - "native:cellstatistics", - { - "INPUT": [sieve_output_updated], - "STATISTIC": 0, - "IGNORE_NODATA": False, - "REFERENCE_LAYER": sieve_output_updated, - "OUTPUT_NODATA_VALUE": -9999, - "OUTPUT": output, - }, - context=self.processing_context, - feedback=self.feedback, - ) - - # self.log_message( - # f"Used parameters for running sieve function to the models: {alg_params} \n" - # ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - model.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem running sieve function on models layers, {e} \n") - self.cancel_task(e) - return False - - return True - - def run_activities_normalization(self, activities, extent, temporary_output=False): - """Runs the normalization analysis on the activities' layers, - adjusting band values measured on different scale, the resulting scale - is computed using the below formula - Normalized_activity = (Carbon coefficient + Suitability index) * ( - (Activity layer value) - (Activity band minimum value)) / - (Activity band maximum value - Activity band minimum value)) - - If the carbon coefficient and suitability index are both zero then - the computation won't take them into account in the normalization - calculation. - - :param activities: List of the analyzed activities - :type activities: typing.List[Activity] - - :param extent: Selected area of interest extent - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - self.set_status_message(tr("Normalization of the activities")) - - try: - for activity in activities: - if activity.path is None or activity.path == "": - if not self.processing_cancelled: - self.set_info_message( - tr( - f"Problem when running activities normalization, " - f"there is no map layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"Problem when running activities normalization, " - f"there is no map layer for the activity {activity.name}" - ) - else: - # If the user cancelled the processing - self.set_info_message( - tr(f"Processing has been cancelled by the user."), - level=Qgis.Critical, - ) - self.log_message(f"Processing has been cancelled by the user.") - - return False - - layers = [] - normalized_activities_directory = os.path.join( - self.scenario_directory, "normalized_activities" - ) - FileUtils.create_new_dir(normalized_activities_directory) - file_name = clean_filename(activity.name.replace(" ", "_")) - - output_file = os.path.join( - normalized_activities_directory, - f"{file_name}_{str(uuid.uuid4())[:4]}.tif", - ) - - activity_layer = QgsRasterLayer(activity.path, activity.name) - provider = activity_layer.dataProvider() - band_statistics = provider.bandStatistics(1) - - min_value = band_statistics.minimumValue - max_value = band_statistics.maximumValue - - self.log_message( - f"Found minimum {min_value} and " - f"maximum {max_value} for activity {activity.name} \n" - ) - - layer_name = Path(activity.path).stem - - layers.append(activity.path) - - carbon_coefficient = float( - self.get_settings_value(Settings.CARBON_COEFFICIENT, default=0.0) - ) - - suitability_index = float( - self.get_settings_value( - Settings.PATHWAY_SUITABILITY_INDEX, default=0 - ) - ) - - normalization_index = carbon_coefficient + suitability_index - - if normalization_index > 0: - expression = ( - f" {normalization_index} * " - f'("{layer_name}@1" - {min_value}) /' - f" ({max_value} - {min_value})" - ) - - else: - expression = ( - f'("{layer_name}@1" - {min_value}) /' - f" ({max_value} - {min_value})" - ) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - alg_params = { - "CELLSIZE": 0, - "CRS": None, - "EXPRESSION": expression, - "EXTENT": extent, - "LAYERS": layers, - "OUTPUT": output, - } - - self.log_message( - f"Used parameters for normalization of the activities: {alg_params} \n" - ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "qgis:rastercalculator", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - activity.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem normalizing activity layers, {e} \n") - self.cancel_task(e) - return False - - return True - - def run_activities_weighting( - self, activities, priority_layers_groups, extent, temporary_output=False - ): - """Runs weighting analysis on the passed activities using - the corresponding activities weight layers. - - :param activities: List of the selected activities - :type activities: typing.List[Activity] - - :param priority_layers_groups: Used priority layers groups and their values - :type priority_layers_groups: dict - - :param extent: selected extent from user - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: A tuple with the weighted activities outputs and - a value of whether the task operations was successful - :rtype: typing.Tuple[typing.List, bool] - """ - - if self.processing_cancelled: - return [], False - - self.set_status_message(tr(f"Weighting activities")) - - weighted_activities = [] - - try: - for original_activity in activities: - activity = clone_activity(original_activity) - - if activity.path is None or activity.path == "": - self.set_info_message( - tr( - f"Problem when running activities weighting, " - f"there is no map layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"Problem when running activities weighting, " - f"there is no map layer for the activity {activity.name}" - ) - - return [], False - - basenames = [] - layers = [] - - layers.append(activity.path) - basenames.append(f'"{Path(activity.path).stem}@1"') - - if not any(priority_layers_groups): - self.log_message( - f"There are no defined priority layers in groups," - f" skipping activities weighting step." - ) - self.run_activities_cleaning( - extent, temporary_output=temporary_output - ) - return - - if activity.priority_layers is None or activity.priority_layers is []: - self.log_message( - f"There are no associated " - f"priority weighting layers for activity {activity.name}" - ) - continue - - settings_activity = self.get_activity(str(activity.uuid)) - - for layer in settings_activity.priority_layers: - if layer is None: - continue - - settings_layer = self.get_priority_layer(layer.get("uuid")) - if settings_layer is None: - continue - - pwl = settings_layer.get("path") - - missing_pwl_message = ( - f"Path {pwl} for priority " - f"weighting layer {layer.get('name')} " - f"doesn't exist, skipping the layer " - f"from the activity {activity.name} weighting." - ) - if pwl is None: - self.log_message(missing_pwl_message) - continue - - pwl_path = Path(pwl) - - if not pwl_path.exists(): - self.log_message(missing_pwl_message) - continue - - path_basename = pwl_path.stem - - for priority_layer in self.get_priority_layers(): - if priority_layer.get("name") == layer.get("name"): - for group in priority_layer.get("groups", []): - value = group.get("value") - coefficient = float(value) - if coefficient > 0: - if pwl not in layers: - layers.append(pwl) - basenames.append( - f'({coefficient}*"{path_basename}@1")' - ) - - if basenames is []: - return [], True - - weighted_activities_directory = os.path.join( - self.scenario_directory, "weighted_activities" - ) - - FileUtils.create_new_dir(weighted_activities_directory) - - file_name = clean_filename(activity.name.replace(" ", "_")) - output_file = os.path.join( - weighted_activities_directory, - f"{file_name}_{str(uuid.uuid4())[:4]}.tif", - ) - expression = " + ".join(basenames) - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - # Actual processing calculation - alg_params = { - "CELLSIZE": 0, - "CRS": None, - "EXPRESSION": expression, - "EXTENT": extent, - "LAYERS": layers, - "OUTPUT": output, - } - - self.log_message( - f" Used parameters for calculating weighting activities {alg_params} \n" - ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return [], False - - results = processing.run( - "qgis:rastercalculator", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - activity.path = results["OUTPUT"] - - weighted_activities.append(activity) - - except Exception as e: - self.log_message(f"Problem weighting activities, {e}\n") - self.cancel_task(e) - return None, False - - return weighted_activities, True - - def run_activities_cleaning(self, activities, extent=None, temporary_output=False): - """Cleans the weighted activities replacing - zero values with no-data as they are not statistical meaningful for the - scenario analysis. - - :param extent: Selected extent from user - :type extent: str - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - """ - - if self.processing_cancelled: - return False - - self.set_status_message(tr("Updating weighted activity values")) - - try: - for activity in activities: - if activity.path is None or activity.path == "": - self.set_info_message( - tr( - f"Problem when running activity updates, " - f"there is no map layer for the activity {activity.name}" - ), - level=Qgis.Critical, - ) - self.log_message( - f"Problem when running activity updates, " - f"there is no map layer for the activity {activity.name}" - ) - - return False - - layers = [activity.path] - - file_name = clean_filename(activity.name.replace(" ", "_")) - - output_file = os.path.join( - self.scenario_directory, "weighted_activities" - ) - output_file = os.path.join( - output_file, f"{file_name}_{str(uuid.uuid4())[:4]}_cleaned.tif" - ) - - # Actual processing calculation - # The aim is to convert pixels values to no data, that is why we are - # using the sum operation with only one layer. - - output = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - alg_params = { - "IGNORE_NODATA": True, - "INPUT": layers, - "EXTENT": extent, - "OUTPUT_NODATA_VALUE": 0, - "REFERENCE_LAYER": layers[0] if len(layers) > 0 else None, - "STATISTIC": 0, # Sum - "OUTPUT": output, - } - - self.log_message( - f"Used parameters for " - f"updates on the weighted activities: {alg_params} \n" - ) - - feedback = QgsProcessingFeedback() - - feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - results = processing.run( - "native:cellstatistics", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - activity.path = results["OUTPUT"] - - except Exception as e: - self.log_message(f"Problem cleaning activities, {e}") - self.cancel_task(e) - return False - - return True - - def run_highest_position_analysis(self, temporary_output=False): - """Runs the highest position analysis which is last step - in scenario analysis. Uses the activities set by the current ongoing - analysis. - - :param temporary_output: Whether to save the processing outputs as temporary - files - :type temporary_output: bool - - :returns: Whether the task operations was successful - :rtype: bool - - """ - if self.processing_cancelled: - # Will not proceed if processing has been cancelled by the user - return False - - passed_extent_box = self.analysis_extent.bbox - passed_extent = QgsRectangle( - passed_extent_box[0], - passed_extent_box[2], - passed_extent_box[1], - passed_extent_box[3], - ) - - self.scenario_result = ScenarioResult( - scenario=self.scenario, scenario_directory=self.scenario_directory - ) - - try: - layers = {} - - self.set_status_message(tr("Calculating the highest position")) - - for activity in self.analysis_weighted_activities: - if activity.path is not None and activity.path != "": - raster_layer = QgsRasterLayer(activity.path, activity.name) - layers[activity.name] = ( - raster_layer if raster_layer is not None else None - ) - else: - for pathway in activity.pathways: - layers[activity.name] = QgsRasterLayer(pathway.path) - - source_crs = QgsCoordinateReferenceSystem("EPSG:4326") - dest_crs = list(layers.values())[0].crs() if len(layers) > 0 else source_crs - - extent_string = ( - f"{passed_extent.xMinimum()},{passed_extent.xMaximum()}," - f"{passed_extent.yMinimum()},{passed_extent.yMaximum()}" - f" [{dest_crs.authid()}]" - ) - - output_file = os.path.join( - self.scenario_directory, - f"{SCENARIO_OUTPUT_FILE_NAME}_{str(self.scenario.uuid)[:4]}.tif", - ) - - # Preparing the input rasters for the highest position - # analysis in a correct order - - activity_names = [ - activity.name for activity in self.analysis_weighted_activities - ] - all_activities = sorted( - self.analysis_weighted_activities, - key=lambda activity_instance: activity_instance.style_pixel_value, - ) - for index, activity in enumerate(all_activities): - activity.style_pixel_value = index + 1 - - all_activity_names = [activity.name for activity in all_activities] - sources = [] - - for activity_name in all_activity_names: - if activity_name in activity_names: - sources.append(layers[activity_name].source()) - - self.log_message( - f"Layers sources {[Path(source).stem for source in sources]}" - ) - - output_file = ( - QgsProcessing.TEMPORARY_OUTPUT if temporary_output else output_file - ) - - alg_params = { - "IGNORE_NODATA": True, - "INPUT_RASTERS": sources, - "EXTENT": extent_string, - "OUTPUT_NODATA_VALUE": -9999, - "REFERENCE_LAYER": list(layers.values())[0] - if len(layers) >= 1 - else None, - "OUTPUT": output_file, - } - - self.log_message( - f"Used parameters for highest position analysis {alg_params} \n" - ) - - self.feedback = QgsProcessingFeedback() - - self.feedback.progressChanged.connect(self.update_progress) - - if self.processing_cancelled: - return False - - self.output = processing.run( - "native:highestpositioninrasterstack", - alg_params, - context=self.processing_context, - feedback=self.feedback, - ) - - except Exception as err: - self.log_message( - tr( - "An error occurred when running task for " - 'scenario analysis, error message "{}"'.format(str(err)) - ) - ) - self.cancel_task(err) - return False - - return True diff --git a/django_project/cplus/tests.py b/django_project/cplus/tests.py deleted file mode 100644 index a39b155..0000000 --- a/django_project/cplus/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/django_project/cplus/utils/__init__.py b/django_project/cplus/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_project/cplus/utils/conf.py b/django_project/cplus/utils/conf.py deleted file mode 100644 index 9e00be5..0000000 --- a/django_project/cplus/utils/conf.py +++ /dev/null @@ -1,1333 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Handles storage and retrieval of the plugin QgsSettings. -""" - -import contextlib -import dataclasses -import datetime -import enum -import json -import os.path -from pathlib import Path -import typing -import uuid - -from qgis.PyQt import QtCore -from qgis.core import QgsRectangle, QgsSettings - -from cplus.definitions.defaults import PRIORITY_LAYERS - -from cplus.definitions.constants import ( - STYLE_ATTRIBUTE, - NCS_CARBON_SEGMENT, - NCS_PATHWAY_SEGMENT, - NPV_COLLECTION_PROPERTY, - PATH_ATTRIBUTE, - PATHWAYS_ATTRIBUTE, - PIXEL_VALUE_ATTRIBUTE, - PRIORITY_LAYERS_SEGMENT, - UUID_ATTRIBUTE, -) - -from cplus.models.base import ( - Activity, - NcsPathway, - Scenario, - ScenarioResult, - SpatialExtent, -) -from cplus.models.financial import ActivityNpvCollection -from cplus.models.helpers import ( - activity_npv_collection_to_dict, - create_activity, - create_activity_npv_collection, - create_ncs_pathway, - layer_component_to_dict, - ncs_pathway_to_dict, -) - -from cplus.utils.helper import log, todict, CustomJsonEncoder - - -@contextlib.contextmanager -def qgis_settings(group_root: str, settings=None): - """Context manager to help defining groups when creating QgsSettings. - - :param group_root: Name of the root group for the settings - :type group_root: str - - :param settings: QGIS settings to use - :type settings: QgsSettings - - :yields: Instance of the created settings - :ytype: QgsSettings - """ - if settings is None: - settings = QgsSettings() - settings.beginGroup(group_root) - try: - yield settings - finally: - settings.endGroup() - - -@dataclasses.dataclass -class ScenarioSettings(Scenario): - """Plugin Scenario settings.""" - - @classmethod - def from_qgs_settings(cls, identifier: str, settings: QgsSettings): - """Reads QGIS settings and parses them into a scenario - settings instance with the respective settings values as properties. - - :param identifier: Scenario identifier - :type identifier: str - - :param settings: Scenario identifier - :type settings: QgsSettings - - :returns: Scenario settings object - :rtype: ScenarioSettings - """ - - activities_list = settings.value("activities", []) - weighted_activities_list = settings.value("activities", []) - - activities = [ - settings_manager.get_activity(activity_uuid) - for activity_uuid in activities_list - ] - weighted_activities = [ - settings_manager.get_activity(activity_uuid) - for activity_uuid in weighted_activities_list - ] - - return cls( - uuid=uuid.UUID(identifier), - name=settings.value("name", None), - description=settings.value("description", None), - extent=[], - activities=activities, - weighted_activities=weighted_activities, - priority_layer_groups=[], - ) - - @classmethod - def get_scenario_extent(cls, identifier): - """Fetches Scenario extent from - the passed scenario settings. - - - :returns: Spatial extent instance extent - :rtype: SpatialExtent - """ - spatial_key = ( - f"{settings_manager._get_scenario_settings_base(identifier)}/extent/spatial" - ) - - with qgis_settings(spatial_key) as settings: - bbox = settings.value("bbox", None) - spatial_extent = SpatialExtent(bbox=bbox) - - return spatial_extent - - -class Settings(enum.Enum): - """Plugin settings names""" - - DOWNLOAD_FOLDER = "download_folder" - REFRESH_FREQUENCY = "refresh/period" - REFRESH_FREQUENCY_UNIT = "refresh/unit" - REFRESH_LAST_UPDATE = "refresh/last_update" - REFRESH_STATE = "refresh/state" - - # Report settings - REPORT_ORGANIZATION = "report/organization" - REPORT_CONTACT_EMAIL = "report/email" - REPORT_WEBSITE = "report/website" - REPORT_CUSTOM_LOGO = "report/custom_logo" - REPORT_CPLUS_LOGO = "report/cplus_logo" - REPORT_CI_LOGO = "report/ci_logo" - REPORT_LOGO_DIR = "report/logo_dir" - REPORT_FOOTER = "report/footer" - REPORT_DISCLAIMER = "report/disclaimer" - REPORT_LICENSE = "report/license" - REPORT_STAKEHOLDERS = "report/stakeholders" - REPORT_CULTURE_POLICIES = "report/culture_policies" - REPORT_CULTURE_CONSIDERATIONS = "report/culture_considerations" - - # Last selected data directory - LAST_DATA_DIR = "last_data_dir" - LAST_MASK_DIR = "last_mask_dir" - - # Advanced settings - BASE_DIR = "advanced/base_dir" - - # Scenario basic details - SCENARIO_NAME = "scenario_name" - SCENARIO_DESCRIPTION = "scenario_description" - SCENARIO_EXTENT = "scenario_extent" - - # Coefficient for carbon layers - CARBON_COEFFICIENT = "carbon_coefficient" - - # Pathway suitability index value - PATHWAY_SUITABILITY_INDEX = "pathway_suitability_index" - - # Snapping values - SNAPPING_ENABLED = "snapping_enabled" - SNAP_LAYER = "snap_layer" - ALLOW_RESAMPLING = "snap_resampling" - RESCALE_VALUES = "snap_rescale" - RESAMPLING_METHOD = "snap_method" - SNAP_PIXEL_VALUE = "snap_pixel_value" - - # Sieve function parameters - SIEVE_ENABLED = "sieve_enabled" - SIEVE_THRESHOLD = "sieve_threshold" - SIEVE_MASK_PATH = "mask_path" - - # Mask layer - MASK_LAYERS_PATHS = "mask_layers_paths" - - # Outputs options - NCS_WITH_CARBON = "ncs_with_carbon" - LANDUSE_PROJECT = "landuse_project" - LANDUSE_NORMALIZED = "landuse_normalized" - LANDUSE_WEIGHTED = "landuse_weighted" - HIGHEST_POSITION = "highest_position" - - # Processing option - PROCESSING_TYPE = "processing_type" - - # DEBUG - DEBUG = "debug" - DEV_MODE = "dev_mode" - BASE_API_URL = "base_api_url" - - -class SettingsManager(QtCore.QObject): - """Manages saving/loading settings for the plugin in QgsSettings.""" - - BASE_GROUP_NAME: str = "cplus_plugin" - SCENARIO_GROUP_NAME: str = "scenarios" - SCENARIO_RESULTS_GROUP_NAME: str = "scenarios_results" - PRIORITY_GROUP_NAME: str = "priority_groups" - PRIORITY_LAYERS_GROUP_NAME: str = "priority_layers" - NCS_PATHWAY_BASE: str = "ncs_pathways" - LAYER_MAPPING_BASE: str = "layer_mapping" - - ACTIVITY_BASE: str = "activities" - - settings = QgsSettings() - - scenarios_settings_updated = QtCore.pyqtSignal() - priority_layers_changed = QtCore.pyqtSignal() - settings_updated = QtCore.pyqtSignal([str, object], [Settings, object]) - - def set_value(self, name: str, value): - """Adds a new setting key and value on the plugin specific settings. - - :param name: Name of setting key - :type name: str - - :param value: Value of the setting - :type value: Any - """ - self.settings.setValue(f"{self.BASE_GROUP_NAME}/{name}", value) - if isinstance(name, Settings): - name = name.value - - self.settings_updated.emit(name, value) - - def get_value(self, name: str, default=None, setting_type=None): - """Gets value of the setting with the passed name. - - :param name: Name of setting key - :type name: str - - :param default: Default value returned when the setting key does not exist - :type default: Any - - :param setting_type: Type of the store setting - :type setting_type: Any - - :returns: Value of the setting - :rtype: Any - """ - if setting_type: - return self.settings.value( - f"{self.BASE_GROUP_NAME}/{name}", default, setting_type - ) - return self.settings.value(f"{self.BASE_GROUP_NAME}/{name}", default) - - def find_settings(self, name): - """Returns the plugin setting keys from the - plugin root group that matches the passed name - - :param name: Setting name to search for - :type name: str - - :returns result: List of the matching settings names - :rtype result: list - """ - - result = [] - with qgis_settings(f"{self.BASE_GROUP_NAME}") as settings: - for settings_name in settings.childKeys(): - if name in settings_name: - result.append(settings_name) - return result - - def remove(self, name): - """Remove the setting with the specified name. - - :param name: Name of the setting key - :type name: str - """ - self.settings.remove(f"{self.BASE_GROUP_NAME}/{name}") - - def delete_settings(self): - """Deletes the all the plugin settings.""" - self.settings.remove(f"{self.BASE_GROUP_NAME}") - - def _get_scenario_settings_base(self, identifier): - """Gets the scenario settings base url. - - :param identifier: Scenario settings identifier - :type identifier: uuid.UUID - - :returns: Scenario settings base group - :rtype: str - """ - return ( - f"{self.BASE_GROUP_NAME}/" - f"{self.SCENARIO_GROUP_NAME}/" - f"{str(identifier)}" - ) - - def _get_scenario_results_settings_base(self, identifier): - """Gets the scenario results settings base url. - - :param identifier: Scenario identifier - :type identifier: uuid.UUID - - :returns: Scenario settings base group - :rtype: str - """ - return ( - f"{self.BASE_GROUP_NAME}/" - f"{self.SCENARIO_RESULTS_GROUP_NAME}/" - f"{str(identifier)}" - ) - - def save_scenario(self, scenario_settings): - """Save the passed scenario settings into the plugin settings - - :param scenario_settings: Scenario settings - :type scenario_settings: ScenarioSettings - """ - settings_key = self._get_scenario_settings_base(scenario_settings.uuid) - - self.save_scenario_extent(settings_key, scenario_settings.extent) - - scenario_activities_ids = [ - str(activity.uuid) for activity in scenario_settings.activities - ] - weighted_activities_ids = [ - str(activity.uuid) for activity in scenario_settings.weighted_activities - ] - - with qgis_settings(settings_key) as settings: - settings.setValue("uuid", scenario_settings.uuid) - settings.setValue("name", scenario_settings.name) - settings.setValue("description", scenario_settings.description) - settings.setValue("activities", scenario_activities_ids) - settings.setValue("weighted_activities", weighted_activities_ids) - - def save_scenario_extent(self, key, extent): - """Saves the scenario extent into plugin settings - using the provided settings group key. - - :param key: Scenario extent - :type key: SpatialExtent - - :param extent: QgsSettings group key - :type extent: str - - Args: - extent (SpatialExtent): Scenario extent - key (str): QgsSettings group key - """ - spatial_extent = extent.bbox - - spatial_key = f"{key}/extent/spatial/" - with qgis_settings(spatial_key) as settings: - settings.setValue("bbox", spatial_extent) - - # def get_scenario(self, identifier): - # """Retrieves the scenario that matches the passed identifier. - # - # :param identifier: Scenario identifier - # :type identifier: str - # - # :returns: Scenario settings instance - # :rtype: ScenarioSettings - # """ - # - # settings_key = self._get_scenario_settings_base(identifier) - # with qgis_settings(settings_key) as settings: - # scenario_settings = ScenarioSettings.from_qgs_settings( - # str(identifier), settings - # ) - # return scenario_settings - - def get_scenario(self, scenario_id): - """Retrieves the first scenario that matched the passed scenario id. - - :param scenario_id: Scenario id - :type scenario_id: str - - :returns: Scenario settings instance - :rtype: ScenarioSettings - """ - - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.SCENARIO_GROUP_NAME}" - ) as settings: - for scenario_uuid in settings.childGroups(): - scenario_settings_key = self._get_scenario_settings_base(scenario_uuid) - with qgis_settings(scenario_settings_key) as scenario_settings: - if scenario_uuid == scenario_id: - scenario = ScenarioSettings.from_qgs_settings( - scenario_uuid, scenario_settings - ) - - scenario.extent = scenario.get_scenario_extent(scenario_uuid) - return scenario - return None - - def get_scenarios(self): - """Gets all the available scenarios settings in the plugin. - - :returns: List of the scenario settings instances - :rtype: list - """ - result = [] - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.SCENARIO_GROUP_NAME}" - ) as settings: - for scenario_uuid in settings.childGroups(): - scenario_settings_key = self._get_scenario_settings_base(scenario_uuid) - with qgis_settings(scenario_settings_key) as scenario_settings: - scenario = ScenarioSettings.from_qgs_settings( - scenario_uuid, scenario_settings - ) - scenario.extent = scenario.get_scenario_extent(scenario_uuid) - result.append(scenario) - return result - - def delete_scenario(self, scenario_id): - """Delete the scenario with the passed scenarion id. - - :param scenario_id: Scenario identifier - :type scenario_id: str - """ - - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.SCENARIO_GROUP_NAME}" - ) as settings: - for scenario_identifier in settings.childGroups(): - if str(scenario_identifier) == str(scenario_id): - settings.remove(scenario_identifier) - - def delete_all_scenarios(self): - """Deletes all the plugin scenarios settings.""" - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.SCENARIO_GROUP_NAME}" - ) as settings: - for scenario_name in settings.childGroups(): - settings.remove(scenario_name) - - def save_scenario_result(self, scenario_result, scenario_id): - """Save the scenario results plugin settings - - :param scenario_settings: Scenario settings - :type scenario_settings: ScenarioSettings - """ - settings_key = self._get_scenario_results_settings_base(scenario_id) - - analysis_output = json.dumps(scenario_result.analysis_output) - - with qgis_settings(settings_key) as settings: - settings.setValue("scenario_id", scenario_id) - settings.setValue( - "created_date", - scenario_result.created_date.strftime("%Y_%m_%d_%H_%M_%S"), - ) - settings.setValue("analysis_output", analysis_output) - settings.setValue("output_layer_name", scenario_result.output_layer_name) - settings.setValue("scenario_directory", scenario_result.scenario_directory) - - def get_scenario_result(self, scenario_id): - """Retrieves the scenario result that matched the passed scenario id. - - :param scenario_id: Scenario id - :type scenario_id: str - - :returns: Scenario result - :rtype: ScenarioSettings - """ - - scenario_settings_key = self._get_scenario_results_settings_base(scenario_id) - with qgis_settings(scenario_settings_key) as scenario_settings: - created_date = scenario_settings.value("created_date") - analysis_output = scenario_settings.value("analysis_output") - output_layer_name = scenario_settings.value("output_layer_name") - scenario_directory = scenario_settings.value("scenario_directory") - - try: - created_date = datetime.datetime.strptime( - created_date, "%Y_%m_%d_%H_%M_%S" - ) - analysis_output = json.loads(analysis_output) - except Exception as e: - log(f"Problem fetching scenario result, {e}") - return None - - return ScenarioResult( - scenario=None, - created_date=created_date, - analysis_output=analysis_output, - output_layer_name=output_layer_name, - scenario_directory=scenario_directory, - ) - return None - - def get_scenarios_results(self): - """Gets all the saved scenarios results. - - :returns: List of the scenario results - :rtype: list - """ - result = [] - with qgis_settings( - f"{self.BASE_GROUP_NAME}/{self.SCENARIO_RESULTS_GROUP_NAME}" - ) as settings: - for uuid in settings.childGroups(): - scenario_settings_key = self._get_scenario_results_settings_base(uuid) - with qgis_settings(scenario_settings_key) as scenario_settings: - created_date = scenario_settings.value("created_date") - analysis_output = scenario_settings.value("analysis_output") - output_layer_name = scenario_settings.value("output_layer_name") - scenario_directory = scenario_settings.value("scenario_directory") - - try: - created_date = datetime.datetime.strptime( - created_date, "%Y_%m_%d_%H_%M_%S" - ) - analysis_output = json.loads(analysis_output) - except Exception as e: - log(f"Problem fetching scenario result, {e}") - return None - - result.append( - ScenarioResult( - scenario=None, - created_date=created_date, - analysis_output=analysis_output, - output_layer_name=output_layer_name, - scenario_directory=scenario_directory, - ) - ) - return result - - def delete_scenario_result(self, scenario_id): - """Delete the scenario result that contains the scenario id. - - :param scenario_id: Scenario identifier - :type scenario_id: str - """ - - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.SCENARIO_RESULTS_GROUP_NAME}" - ) as settings: - for scenario_identifier in settings.childGroups(): - if str(scenario_identifier) == str(scenario_id): - settings.remove(scenario_identifier) - - def delete_all_scenarios_results(self): - """Deletes all the plugin scenarios results settings.""" - with qgis_settings( - f"{self.BASE_GROUP_NAME}/{self.SCENARIO_GROUP_NAME}/" - f"{self.SCENARIO_RESULTS_GROUP_NAME}" - ) as settings: - for scenario_result in settings.childGroups(): - settings.remove(scenario_result) - - def _get_priority_layers_settings_base(self, identifier) -> str: - """Gets the priority layers settings base url. - - :param identifier: Priority layers settings identifier - :type identifier: uuid.UUID - - :returns: Priority layers settings base group - :rtype: str - """ - return ( - f"{self.BASE_GROUP_NAME}/" - f"{self.PRIORITY_LAYERS_GROUP_NAME}/" - f"{str(identifier)}" - ) - - def get_priority_layer(self, identifier) -> typing.Dict: - """Retrieves the priority layer that matches the passed identifier. - - :param identifier: Priority layers identifier - :type identifier: uuid.UUID - - :returns: Priority layer dict - :rtype: dict - """ - priority_layer = None - - settings_key = self._get_priority_layers_settings_base(identifier) - with qgis_settings(settings_key) as settings: - groups_key = f"{settings_key}/groups" - groups = [] - - if len(settings.childKeys()) <= 0: - return priority_layer - - with qgis_settings(groups_key) as groups_settings: - for name in groups_settings.childGroups(): - group_settings_key = f"{groups_key}/{name}" - with qgis_settings(group_settings_key) as group_settings: - stored_group = {} - stored_group["uuid"] = group_settings.value("uuid") - stored_group["name"] = group_settings.value("name") - stored_group["value"] = group_settings.value("value") - groups.append(stored_group) - - priority_layer = {"uuid": str(identifier)} - priority_layer["name"] = settings.value("name") - priority_layer["description"] = settings.value("description") - priority_layer["path"] = settings.value("path") - priority_layer["selected"] = settings.value("selected", type=bool) - priority_layer["user_defined"] = settings.value( - "user_defined", defaultValue=True, type=bool - ) - priority_layer["type"] = settings.value("type", defaultValue=0, type=int) - priority_layer["groups"] = groups - return priority_layer - - def get_priority_layers(self) -> typing.List: - """Gets all the available priority layers in the plugin. - - :returns: Priority layers list - :rtype: list - """ - priority_layer_list = [] - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}" - ) as settings: - for uuid in settings.childGroups(): - priority_layer_settings = self._get_priority_layers_settings_base(uuid) - with qgis_settings(priority_layer_settings) as priority_settings: - groups_key = f"{priority_layer_settings}/groups" - groups = [] - - with qgis_settings(groups_key) as groups_settings: - for name in groups_settings.childGroups(): - group_settings_key = f"{groups_key}/{name}" - with qgis_settings(group_settings_key) as group_settings: - stored_group = {} - stored_group["uuid"] = group_settings.value("uuid") - stored_group["name"] = group_settings.value("name") - stored_group["value"] = group_settings.value("value") - groups.append(stored_group) - layer = { - "uuid": uuid, - "name": priority_settings.value("name"), - "description": priority_settings.value("description"), - "path": priority_settings.value("path"), - "selected": priority_settings.value("selected", type=bool), - "user_defined": priority_settings.value( - "user_defined", defaultValue=True, type=bool - ), - "type": priority_settings.value( - "type", defaultValue=0, type=int - ), - "groups": groups, - } - priority_layer_list.append(layer) - return priority_layer_list - - def find_layer_by_name(self, name) -> typing.Dict: - """Finds a priority layer setting inside - the plugin QgsSettings by name. - - :param name: Priority layers identifier - :type name: str - - :returns: Priority layers dict - :rtype: dict - """ - found_id = None - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}" - ) as settings: - for layer_id in settings.childGroups(): - layer_settings_key = self._get_priority_layers_settings_base(layer_id) - with qgis_settings(layer_settings_key) as layer_settings: - layer_name = layer_settings.value("name") - if layer_name == name: - found_id = uuid.UUID(layer_id) - break - - return self.get_priority_layer(found_id) if found_id is not None else None - - def find_layers_by_group(self, group) -> typing.List: - """Finds priority layers inside the plugin QgsSettings - that contain the passed group. - - :param group: Priority group name - :type group: str - - :returns: Priority layers list - :rtype: list - """ - layers = [] - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}" - ) as settings: - for layer_id in settings.childGroups(): - priority_layer_settings = self._get_priority_layers_settings_base( - layer_id - ) - with qgis_settings(priority_layer_settings) as priority_settings: - groups_key = f"{priority_layer_settings}/groups" - - with qgis_settings(groups_key) as groups_settings: - for name in groups_settings.childGroups(): - group_settings_key = f"{groups_key}/{name}" - with qgis_settings(group_settings_key) as group_settings: - if group == group_settings.value("name"): - layers.append(self.get_priority_layer(layer_id)) - return layers - - def save_priority_layer(self, priority_layer): - """Save the priority layer into the plugin settings. - Updates the layer with new priority groups. - - Note: Emits priority_layers_changed signal - - :param priority_layer: Priority layer - :type priority_layer: dict - """ - settings_key = self._get_priority_layers_settings_base(priority_layer["uuid"]) - - with qgis_settings(settings_key) as settings: - groups = priority_layer.get("groups", []) - settings.setValue("name", priority_layer["name"]) - settings.setValue("description", priority_layer["description"]) - settings.setValue("path", priority_layer["path"]) - settings.setValue("selected", priority_layer.get("selected", False)) - settings.setValue("user_defined", priority_layer.get("user_defined", True)) - settings.setValue("type", priority_layer.get("type", 0)) - groups_key = f"{settings_key}/groups" - with qgis_settings(groups_key) as groups_settings: - for group_id in groups_settings.childGroups(): - groups_settings.remove(group_id) - for group in groups: - group_key = f"{groups_key}/{group['name']}" - with qgis_settings(group_key) as group_settings: - group_settings.setValue("uuid", str(group.get("uuid"))) - group_settings.setValue("name", group["name"]) - group_settings.setValue("value", group["value"]) - - self.priority_layers_changed.emit() - - def set_current_priority_layer(self, identifier): - """Set current priority layer - - :param identifier: Priority layer identifier - :type identifier: str - """ - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}/" - ) as settings: - for priority_layer in settings.childGroups(): - settings_key = self._get_priority_layers_settings_base(identifier) - with qgis_settings(settings_key) as layer_settings: - layer_settings.setValue( - "selected", str(priority_layer) == str(identifier) - ) - - def delete_priority_layers(self): - """Deletes all the plugin priority weighting layers settings.""" - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}" - ) as settings: - for priority_layer in settings.childGroups(): - settings.remove(priority_layer) - - def delete_priority_layer(self, identifier): - """Removes priority layer that match the passed identifier - - :param identifier: Priority layer identifier - :type identifier: str - """ - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_LAYERS_GROUP_NAME}/" - ) as settings: - for priority_layer in settings.childGroups(): - if str(priority_layer) == str(identifier): - settings.remove(priority_layer) - - def _get_priority_groups_settings_base(self, identifier) -> str: - """Gets the priority group settings base url. - - :param identifier: Priority group settings identifier - :type identifier: str - - :returns: Priority groups settings base group - :rtype: str - - """ - return ( - f"{self.BASE_GROUP_NAME}/" - f"{self.PRIORITY_GROUP_NAME}/" - f"{str(identifier)}" - ) - - def find_group_by_name(self, name) -> typing.Dict: - """Finds a priority group setting inside the plugin QgsSettings by name. - - :param name: Name of the group - :type name: str - - :returns: Priority group - :rtype: typing.Dict - """ - - found_id = None - - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_GROUP_NAME}" - ) as settings: - for group_id in settings.childGroups(): - group_settings_key = self._get_priority_groups_settings_base(group_id) - with qgis_settings(group_settings_key) as group_settings_key: - group_name = group_settings_key.value("name") - if group_name == name: - found_id = uuid.UUID(group_id) - break - - return self.get_priority_group(found_id) - - def get_priority_group(self, identifier) -> typing.Dict: - """Retrieves the priority group that matches the passed identifier. - - :param identifier: Priority group identifier - :type identifier: str - - :returns: Priority group - :rtype: typing.Dict - """ - - if identifier is None: - return None - - settings_key = self._get_priority_groups_settings_base(identifier) - with qgis_settings(settings_key) as settings: - priority_group = {"uuid": identifier} - priority_group["name"] = settings.value("name") - priority_group["value"] = settings.value("value") - priority_group["description"] = settings.value("description") - return priority_group - - def get_priority_groups(self) -> typing.List[typing.Dict]: - """Gets all the available priority groups in the plugin. - - :returns: List of the priority groups instances - :rtype: list - """ - priority_groups = [] - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_GROUP_NAME}" - ) as settings: - for uuid in settings.childGroups(): - priority_layer_settings = self._get_priority_groups_settings_base(uuid) - with qgis_settings(priority_layer_settings) as priority_settings: - group = { - "uuid": uuid, - "name": priority_settings.value("name"), - "value": priority_settings.value("value"), - "description": priority_settings.value("description"), - } - priority_groups.append(group) - return priority_groups - - def save_priority_group(self, priority_group): - """Save the priority group into the plugin settings - - :param priority_group: Priority group - :type priority_group: str - """ - - settings_key = self._get_priority_groups_settings_base(priority_group["uuid"]) - - with qgis_settings(settings_key) as settings: - settings.setValue("name", priority_group["name"]) - settings.setValue("value", priority_group["value"]) - settings.setValue("description", priority_group.get("description")) - - def delete_priority_group(self, identifier): - """Removes priority group that match the passed identifier - - :param identifier: Priority group identifier - :type identifier: str - """ - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_GROUP_NAME}/" - ) as settings: - for priority_group in settings.childGroups(): - if str(priority_group) == str(identifier): - settings.remove(priority_group) - - def delete_priority_groups(self): - """Deletes all the plugin priority groups settings.""" - with qgis_settings( - f"{self.BASE_GROUP_NAME}/" f"{self.PRIORITY_GROUP_NAME}" - ) as settings: - for priority_group in settings.childGroups(): - settings.remove(priority_group) - - def _get_layer_mappings_settings_base(self) -> str: - """Returns the path for Layer Mapping settings. - - :returns: Base path to Layer Mapping group. - :rtype: str - """ - return f"{self.BASE_GROUP_NAME}/{self.LAYER_MAPPING_BASE}" - - def get_all_layer_mapping(self) -> typing.Dict: - """Return all layer mapping.""" - layer_mapping = {} - - layer_mapping_root = self._get_layer_mappings_settings_base() - with qgis_settings(layer_mapping_root) as settings: - keys = settings.childKeys() - for k in keys: - layer_raw = settings.value(k, dict()) - if len(layer_raw) > 0: - try: - layer = json.loads(layer_raw) - layer_mapping[k] = layer - except json.JSONDecodeError: - log("Layer Mapping JSON is invalid") - return layer_mapping - - def get_layer_mapping(self, identifier) -> typing.Dict: - """Retrieves the layer mapping that matches the passed identifier. - - :param identifier: Layer mapping identifier - :type identifier: str path - - :returns: Layer mapping - :rtype: typing.Dict - """ - - layer_mapping = {} - - layer_mapping_root = self._get_layer_mappings_settings_base() - - with qgis_settings(layer_mapping_root) as settings: - layer = settings.value(identifier, dict()) - if len(layer) > 0: - try: - layer_mapping = json.loads(layer) - except json.JSONDecodeError: - log("Layer Mapping JSON is invalid") - return layer_mapping - - def save_layer_mapping(self, input_layer, identifier=None): - """Save the layer mapping into the plugin settings - - :param input_layer: Layer mapping - :type input_layer: dict - :param identifier: file identifier using path - :type identifier: str - """ - - if not identifier: - identifier = input_layer["path"].replace(os.sep, "--") - settings_key = self._get_layer_mappings_settings_base() - - with qgis_settings(settings_key) as settings: - settings.setValue(identifier, json.dumps(input_layer)) - - def remove_layer_mapping(self, identifier): - """Remove layer mapping from settings.""" - self.remove(f"{self.LAYER_MAPPING_BASE}/{identifier}") - - def _get_ncs_pathway_settings_base(self) -> str: - """Returns the path for NCS pathway settings. - - :returns: Base path to NCS pathway group. - :rtype: str - """ - return f"{self.BASE_GROUP_NAME}/" f"{NCS_PATHWAY_SEGMENT}" - - def save_ncs_pathway(self, ncs_pathway: typing.Union[NcsPathway, dict]): - """Saves an NCS pathway object serialized to a json string - indexed by the UUID. - - :param ncs_pathway: NCS pathway object or attribute values - in a dictionary which are then serialized to a JSON string. - :type ncs_pathway: NcsPathway, dict - """ - if isinstance(ncs_pathway, NcsPathway): - ncs_pathway = ncs_pathway_to_dict(ncs_pathway) - - ncs_str = json.dumps(ncs_pathway) - - ncs_uuid = ncs_pathway[UUID_ATTRIBUTE] - ncs_root = self._get_ncs_pathway_settings_base() - - with qgis_settings(ncs_root) as settings: - settings.setValue(ncs_uuid, ncs_str) - - def get_ncs_pathway(self, ncs_uuid: str) -> typing.Union[NcsPathway, None]: - """Gets an NCS pathway object matching the given unique identified. - - :param ncs_uuid: Unique identifier for the NCS pathway object. - :type ncs_uuid: str - - :returns: Returns the NCS pathway object matching the given - identifier else None if not found. - :rtype: NcsPathway - """ - ncs_pathway = None - - ncs_dict = self.get_ncs_pathway_dict(ncs_uuid) - if len(ncs_dict) == 0: - return None - - ncs_pathway = create_ncs_pathway(ncs_dict) - - return ncs_pathway - - def get_ncs_pathway_dict(self, ncs_uuid: str) -> dict: - """Gets an NCS pathway attribute values as a dictionary. - - :param ncs_uuid: Unique identifier for the NCS pathway object. - :type ncs_uuid: str - - :returns: Returns the NCS pathway attribute values matching the given - identifier else an empty dictionary if not found. - :rtype: dict - """ - ncs_pathway_dict = {} - - ncs_root = self._get_ncs_pathway_settings_base() - - with qgis_settings(ncs_root) as settings: - ncs_model = settings.value(ncs_uuid, dict()) - if len(ncs_model) > 0: - try: - ncs_pathway_dict = json.loads(ncs_model) - except json.JSONDecodeError: - log("NCS pathway JSON is invalid") - - return ncs_pathway_dict - - def get_all_ncs_pathways(self) -> typing.List[NcsPathway]: - """Get all the NCS pathway objects stored in settings. - - :returns: Returns all the NCS pathway objects. - :rtype: list - """ - ncs_pathways = [] - - ncs_root = self._get_ncs_pathway_settings_base() - - with qgis_settings(ncs_root) as settings: - keys = settings.childKeys() - for k in keys: - ncs_pathway = self.get_ncs_pathway(k) - if ncs_pathway is not None: - ncs_pathways.append(ncs_pathway) - - return sorted(ncs_pathways, key=lambda ncs: ncs.name) - - def update_ncs_pathways(self): - """Updates the path attribute of all NCS pathway settings - based on the BASE_DIR settings to reflect the absolute path - of each NCS pathway layer. - If BASE_DIR is empty then the NCS pathway settings will not - be updated. - """ - ncs_pathways = self.get_all_ncs_pathways() - for ncs in ncs_pathways: - self.update_ncs_pathway(ncs) - - def update_ncs_pathway(self, ncs_pathway: NcsPathway): - """Updates the attributes of the NCS pathway object - in settings. On the path, the BASE_DIR in settings - is used to reflect the absolute path of each NCS - pathway layer. If BASE_DIR is empty then the NCS - pathway setting will not be updated, this only applies - for default pathways. - - :param ncs_pathway: NCS pathway object to be updated. - :type ncs_pathway: NcsPathway - """ - base_dir = self.get_value(Settings.BASE_DIR) - if not base_dir: - return - - # Pathway location for default pathway - if not ncs_pathway.user_defined: - p = Path(ncs_pathway.path) - # Only update if path does not exist otherwise - # fallback to check under base directory. - if not p.exists(): - abs_path = f"{base_dir}/{NCS_PATHWAY_SEGMENT}/" f"{p.name}" - abs_path = str(os.path.normpath(abs_path)) - ncs_pathway.path = abs_path - - # Carbon location - abs_carbon_paths = [] - for cb_path in ncs_pathway.carbon_paths: - cp = Path(cb_path) - # Similarly, if the given carbon path does not exist then try - # to use the default one in the ncs_carbon directory. - if not cp.exists(): - abs_carbon_path = f"{base_dir}/{NCS_CARBON_SEGMENT}/" f"{cp.name}" - abs_carbon_path = str(os.path.normpath(abs_carbon_path)) - abs_carbon_paths.append(abs_carbon_path) - else: - abs_carbon_paths.append(cb_path) - - ncs_pathway.carbon_paths = abs_carbon_paths - - # Remove then re-insert - self.remove_ncs_pathway(str(ncs_pathway.uuid)) - self.save_ncs_pathway(ncs_pathway) - - def remove_ncs_pathway(self, ncs_uuid: str): - """Removes an NCS pathway settings entry using the UUID. - - :param ncs_uuid: Unique identifier of the NCS pathway entry - to removed. - :type ncs_uuid: str - """ - if self.get_ncs_pathway(ncs_uuid) is not None: - self.remove(f"{self.NCS_PATHWAY_BASE}/{ncs_uuid}") - - def _get_activity_settings_base(self) -> str: - """Returns the path for activity settings. - - :returns: Base path to activity group. - :rtype: str - """ - return f"{self.BASE_GROUP_NAME}/" f"{self.ACTIVITY_BASE}" - - def save_activity(self, activity: typing.Union[Activity, dict]): - """Saves an activity object serialized to a json string - indexed by the UUID. - - :param activity: Activity object or attribute - values in a dictionary which are then serialized to a JSON string. - :type activity: Activity, dict - """ - if isinstance(activity, Activity): - priority_layers = activity.priority_layers - layer_styles = activity.layer_styles - style_pixel_value = activity.style_pixel_value - - ncs_pathways = [] - for ncs in activity.pathways: - ncs_pathways.append(str(ncs.uuid)) - - activity = layer_component_to_dict(activity) - activity[PRIORITY_LAYERS_SEGMENT] = priority_layers - activity[PATHWAYS_ATTRIBUTE] = ncs_pathways - activity[STYLE_ATTRIBUTE] = layer_styles - activity[PIXEL_VALUE_ATTRIBUTE] = style_pixel_value - - if isinstance(activity, dict): - priority_layers = [] - if activity.get("pwls_ids") is not None: - for layer_id in activity.get("pwls_ids", []): - layer = self.get_priority_layer(layer_id) - priority_layers.append(layer) - if len(priority_layers) > 0: - activity[PRIORITY_LAYERS_SEGMENT] = priority_layers - - activity_str = json.dumps(todict(activity), cls=CustomJsonEncoder) - - activity_uuid = activity[UUID_ATTRIBUTE] - activity_root = self._get_activity_settings_base() - - with qgis_settings(activity_root) as settings: - settings.setValue(activity_uuid, activity_str) - - def get_activity(self, activity_uuid: str) -> typing.Union[Activity, None]: - """Gets an activity object matching the given unique - identifier. - - :param activity_uuid: Unique identifier of the - activity object. - :type activity_uuid: str - - :returns: Returns the activity object matching the given - identifier else None if not found. - :rtype: Activity - """ - activity = None - - activity_root = self._get_activity_settings_base() - - with qgis_settings(activity_root) as settings: - activity = settings.value(activity_uuid, None) - ncs_uuids = [] - if activity is not None: - activity_dict = {} - try: - activity_dict = json.loads(activity) - except json.JSONDecodeError: - log("Activity JSON is invalid.") - - if PATHWAYS_ATTRIBUTE in activity_dict: - ncs_uuids = activity_dict[PATHWAYS_ATTRIBUTE] - - activity = create_activity(activity_dict) - if activity is not None: - for ncs_uuid in ncs_uuids: - ncs = self.get_ncs_pathway(ncs_uuid) - if ncs is not None: - activity.add_ncs_pathway(ncs) - - return activity - - def find_activity_by_name(self, name) -> typing.Dict: - """Finds an activity setting inside - the plugin QgsSettings that equals or matches the name. - - :param name: Activity name. - :type name: str - - :returns: Activity object. - :rtype: Activity - """ - for activity in self.get_all_activities(): - model_name = activity.name - trimmed_name = model_name.replace(" ", "_") - if model_name == name or model_name in name or trimmed_name in name: - return activity - - return None - - def get_all_activities(self) -> typing.List[Activity]: - """Get all the activity objects stored in settings. - - :returns: Returns all the activity objects. - :rtype: list - """ - activities = [] - - activity_root = self._get_activity_settings_base() - - with qgis_settings(activity_root) as settings: - keys = settings.childKeys() - for k in keys: - activity = self.get_activity(k) - if activity is not None: - activities.append(activity) - - return sorted(activities, key=lambda activity: activity.name) - - def update_activity(self, activity: Activity): - """Updates the attributes of the activity object - in settings. On the path, the BASE_DIR in settings - is used to reflect the absolute path of each NCS - pathway layer. If BASE_DIR is empty then the NCS - pathway setting will not be updated. - - :param activity: Activity object to be updated. - :type activity: Activity - """ - base_dir = self.get_value(Settings.BASE_DIR) - - if base_dir: - # PWLs path update - for layer in activity.priority_layers: - if layer in PRIORITY_LAYERS and base_dir not in layer.get( - PATH_ATTRIBUTE - ): - abs_pwl_path = ( - f"{base_dir}/{PRIORITY_LAYERS_SEGMENT}/" - f"{layer.get(PATH_ATTRIBUTE)}" - ) - abs_pwl_path = str(os.path.normpath(abs_pwl_path)) - layer[PATH_ATTRIBUTE] = abs_pwl_path - - # Remove then re-insert - self.remove_activity(str(activity.uuid)) - self.save_activity(activity) - - def update_activities(self): - """Updates the attributes of the existing activities.""" - activities = self.get_all_activities() - - for activity in activities: - self.update_activity(activity) - - def remove_activity(self, activity_uuid: str): - """Removes an activity settings entry using the UUID. - - :param activity_uuid: Unique identifier of the activity - to be removed. - :type activity_uuid: str - """ - if self.get_activity(activity_uuid) is not None: - self.remove(f"{self.ACTIVITY_BASE}/{activity_uuid}") - - def get_npv_collection(self) -> typing.Optional[ActivityNpvCollection]: - """Gets the collection of NPV mappings of activities. - - :returns: The collection of activity NPV mappings or None - if not defined. - :rtype: ActivityNpvCollection - """ - npv_collection_str = self.get_value(NPV_COLLECTION_PROPERTY, None) - if not npv_collection_str: - return None - - npv_collection_dict = {} - try: - npv_collection_dict = json.loads(npv_collection_str) - except json.JSONDecodeError: - log("ActivityNPVCollection JSON is invalid.") - - return create_activity_npv_collection( - npv_collection_dict, self.get_all_activities() - ) - - def save_npv_collection(self, npv_collection: ActivityNpvCollection): - """Saves the activity NPV collection in the settings as a serialized - JSON string. - - :param npv_collection: Activity NPV collection serialized to a JSON string. - :type npv_collection: ActivityNpvCollection - """ - npv_collection_dict = activity_npv_collection_to_dict(npv_collection) - npv_collection_str = json.dumps(npv_collection_dict) - self.set_value(NPV_COLLECTION_PROPERTY, npv_collection_str) - - -settings_manager = SettingsManager() diff --git a/django_project/cplus/utils/helper.py b/django_project/cplus/utils/helper.py deleted file mode 100644 index d771988..0000000 --- a/django_project/cplus/utils/helper.py +++ /dev/null @@ -1,619 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Plugin utilities -""" - - -import os -import uuid -import json -import datetime -from pathlib import Path -from uuid import UUID - -from qgis.PyQt import QtCore, QtGui -from qgis.core import ( - Qgis, - QgsCoordinateReferenceSystem, - QgsCoordinateTransform, - QgsCoordinateTransformContext, - QgsDistanceArea, - QgsMessageLog, - QgsProcessingFeedback, - QgsProject, - QgsProcessing, - QgsRasterLayer, - QgsRectangle, - QgsUnitTypes, -) - -from qgis.analysis import QgsAlignRaster - -from qgis import processing - -from cplus.definitions.defaults import DOCUMENTATION_SITE, REPORT_FONT_NAME, TEMPLATE_NAME -from cplus.definitions.constants import ( - NCS_CARBON_SEGMENT, - NCS_PATHWAY_SEGMENT, - PRIORITY_LAYERS_SEGMENT, -) - - -def tr(message): - """Get the translation for a string using Qt translation API. - We implement this ourselves since we do not inherit QObject. - - :param message: String for translation. - :type message: str, QString - - :returns: Translated version of message. - :rtype: QString - """ - # noinspection PyTypeChecker,PyArgumentList,PyCallByClass - return QtCore.QCoreApplication.translate("QgisCplus", message) - - -def log( - message: str, - name: str = "qgis_cplus", - info: bool = True, - notify: bool = True, -): - """Logs the message into QGIS logs using qgis_cplus as the default - log instance. - If notify_user is True, user will be notified about the log. - - :param message: The log message - :type message: str - - :param name: Name of te log instance, qgis_cplus is the default - :type message: str - - :param info: Whether the message is about info or a - warning - :type info: bool - - :param notify: Whether to notify user about the log - :type notify: bool - """ - level = Qgis.Info if info else Qgis.Warning - QgsMessageLog.logMessage( - message, - name, - level=level, - notifyUser=notify, - ) - - -def open_documentation(url=None): - """Opens documentation website in the default browser - - :param url: URL link to documentation site (e.g. gh pages site) - :type url: str - - """ - url = DOCUMENTATION_SITE if url is None else url - result = QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) - return result - - -def get_plugin_version() -> [str, None]: - """Returns the current plugin version - as saved in the metadata.txt plugin file. - - :returns version: Plugin version - :rtype version: str - """ - metadata_file = Path(__file__).parent.resolve() / "metadata.txt" - - with open(metadata_file, "r") as f: - for line in f.readlines(): - if line.startswith("version"): - version = line.strip().split("=")[1] - return version - return None - - -def get_report_font(size=11.0, bold=False, italic=False) -> QtGui.QFont: - """Uses the default font family name to create a - font for use in the report. - - :param size: The font point size, default is 11. - :type size: float - - :param bold: True for bold font else False which is the default. - :type bold: bool - - :param italic: True for font to be in italics else False which is the default. - :type italic: bool - - :returns: Font to use in a report. - :rtype: QtGui.QFont - """ - font_weight = 50 - if bold is True: - font_weight = 75 - - return QtGui.QFont(REPORT_FONT_NAME, int(size), font_weight, italic) - - -def clean_filename(filename): - """Creates a safe filename by removing operating system - invalid filename characters. - - :param filename: File name - :type filename: str - - :returns A clean file name - :rtype str - """ - characters = " %:/,\[]<>*?" - - for character in characters: - if character in filename: - filename = filename.replace(character, "_") - - return filename - - -def calculate_raster_value_area( - layer: QgsRasterLayer, band_number: int = 1, feedback: QgsProcessingFeedback = None -) -> dict: - """Calculates the area of value pixels for the given band in a raster layer. - - Please note that this function will run in the main application thread hence - for best results, it is recommended to execute it in a background process - if part of a bigger workflow. - - :param layer: Input layer whose area for value pixels is to be calculated. - :type layer: QgsRasterLayer - - :param band_number: Band number to compute area, default is band one. - :type band_number: int - - :param feedback: Feedback object for progress during area calculation. - :type feedback: QgsProcessingFeedback - - :returns: A dictionary containing the pixel value as - the key and the corresponding area in hectares as the value for all the pixels - in the raster otherwise returns a empty dictionary if the raster is invalid - or if it is empty. - :rtype: float - """ - if not layer.isValid(): - log("Invalid layer for raster area calculation.", info=False) - return {} - - algorithm_name = "native:rasterlayeruniquevaluesreport" - params = { - "INPUT": layer, - "BAND": band_number, - "OUTPUT_TABLE": "TEMPORARY_OUTPUT", - "OUTPUT_HTML_FILE": QgsProcessing.TEMPORARY_OUTPUT, - } - - algorithm_result = processing.run(algorithm_name, params, feedback=feedback) - - # Get number of pixels with values - total_pixel_count = algorithm_result["TOTAL_PIXEL_COUNT"] - if total_pixel_count == 0: - log("Input layer for raster area calculation is empty.", info=False) - return {} - - output_table = algorithm_result["OUTPUT_TABLE"] - if output_table is None: - log("Unique values raster table could not be retrieved.", info=False) - return {} - - area_calc = QgsDistanceArea() - crs = layer.crs() - area_calc.setSourceCrs(crs, QgsCoordinateTransformContext()) - if crs is not None: - # Use ellipsoid calculation if available - area_calc.setEllipsoid(crs.ellipsoidAcronym()) - - version = Qgis.versionInt() - if version < 33000: - unit_type = QgsUnitTypes.AreaUnit.AreaHectares - else: - unit_type = Qgis.AreaUnit.Hectares - - pixel_areas = {} - features = output_table.getFeatures() - for f in features: - pixel_value = f.attribute(0) - area = f.attribute(2) - pixel_value_area = area_calc.convertAreaMeasurement(area, unit_type) - pixel_areas[pixel_value] = pixel_value_area - - return pixel_areas - - -def transform_extent(extent, source_crs, dest_crs): - """Transforms the passed extent into the destination crs - - :param extent: Target extent - :type extent: QgsRectangle - - :param source_crs: Source CRS of the passed extent - :type source_crs: QgsCoordinateReferenceSystem - - :param dest_crs: Destination CRS - :type dest_crs: QgsCoordinateReferenceSystem - """ - - transform = QgsCoordinateTransform(source_crs, dest_crs, QgsProject.instance()) - transformed_extent = transform.transformBoundingBox(extent) - - return transformed_extent - - -def align_rasters( - input_raster_source, - reference_raster_source, - extent=None, - output_dir=None, - rescale_values=False, - resample_method=0, -): - """ - Based from work on https://github.com/inasafe/inasafe/pull/2070 - Aligns the passed raster files source and save the results into new files. - - :param input_raster_source: Input layer source - :type input_raster_source: str - - :param reference_raster_source: Reference layer source - :type reference_raster_source: str - - :param extent: Clip extent - :type extent: list - - :param output_dir: Absolute path of the output directory for the snapped - layers - :type output_dir: str - - :param rescale_values: Whether to rescale pixel values - :type rescale_values: bool - - :param resample_method: Method to use when resampling - :type resample_method: QgsAlignRaster.ResampleAlg - - """ - - try: - snap_directory = os.path.join(output_dir, "snap_layers") - - FileUtils.create_new_dir(snap_directory) - - input_path = Path(input_raster_source) - - input_layer_output = os.path.join( - f"{snap_directory}", f"{input_path.stem}_{str(uuid.uuid4())[:4]}.tif" - ) - - FileUtils.create_new_file(input_layer_output) - - align = QgsAlignRaster() - lst = [ - QgsAlignRaster.Item(input_raster_source, input_layer_output), - ] - - resample_method_value = QgsAlignRaster.ResampleAlg.RA_NearestNeighbour - - try: - resample_method_value = QgsAlignRaster.ResampleAlg(int(resample_method)) - except Exception as e: - log(f"Problem creating a resample value when snapping, {e}") - - if rescale_values: - lst[0].rescaleValues = rescale_values - - lst[0].resample_method = resample_method_value - - align.setRasters(lst) - align.setParametersFromRaster(reference_raster_source) - - layer = QgsRasterLayer(input_raster_source, "input_layer") - - extent = transform_extent( - layer.extent(), - QgsCoordinateReferenceSystem(layer.crs()), - QgsCoordinateReferenceSystem(align.destinationCrs()), - ) - - align.setClipExtent(extent) - - log(f"Snapping clip extent {layer.extent().asWktPolygon()} \n") - - if not align.run(): - log( - f"Problem during snapping for {input_raster_source} and " - f"{reference_raster_source}, {align.errorMessage()}" - ) - raise Exception(align.errorMessage()) - except Exception as e: - log( - f"Problem occured when snapping, {str(e)}." - f" Update snap settings and re-run the analysis" - ) - - return None, None - - log( - f"Finished snapping" - f" original layer - {input_raster_source}," - f"snapped output - {input_layer_output} \n" - ) - - return input_layer_output, None - - -class FileUtils: - """ - Provides functionality for commonly used file-related operations. - """ - - @staticmethod - def plugin_dir() -> str: - """Returns the root directory of the plugin. - - :returns: Root directory of the plugin. - :rtype: str - """ - return os.path.join(os.path.dirname(os.path.realpath(__file__))) - - @staticmethod - def get_icon(file_name: str) -> QtGui.QIcon: - """Creates an icon based on the icon name in the 'icons' folder. - - :param file_name: File name which should include the extension. - :type file_name: str - - :returns: Icon object matching the file name. - :rtype: QtGui.QIcon - """ - icon_path = os.path.normpath(f"{FileUtils.plugin_dir()}/icons/{file_name}") - - if not os.path.exists(icon_path): - return QtGui.QIcon() - - return QtGui.QIcon(icon_path) - - @staticmethod - def report_template_path(file_name=None) -> str: - """Get the absolute path to the template file with the given name. - Caller needs to verify that the file actually exists. - - :param file_name: Template file name including the extension. If - none is specified then it will use `main.qpt` as the default - template name. - :type file_name: str - - :returns: The absolute path to the template file with the given name. - :rtype: str - """ - if file_name is None: - file_name = TEMPLATE_NAME - - absolute_path = f"{FileUtils.plugin_dir()}/data/reports/{file_name}" - - return os.path.normpath(absolute_path) - - @staticmethod - def create_ncs_pathways_dir(base_dir: str): - """Creates an NCS subdirectory under BASE_DIR. Skips - creation of the subdirectory if it already exists. - """ - if not Path(base_dir).is_dir(): - return - - ncs_pathway_dir = f"{base_dir}/{NCS_PATHWAY_SEGMENT}" - message = tr( - "Missing parent directory when creating NCS pathways subdirectory." - ) - FileUtils.create_new_dir(ncs_pathway_dir, message) - - @staticmethod - def create_ncs_carbon_dir(base_dir: str): - """Creates an NCS subdirectory for carbon layers under BASE_DIR. - Skips creation of the subdirectory if it already exists. - """ - if not Path(base_dir).is_dir(): - return - - ncs_carbon_dir = f"{base_dir}/{NCS_CARBON_SEGMENT}" - message = tr("Missing parent directory when creating NCS carbon subdirectory.") - FileUtils.create_new_dir(ncs_carbon_dir, message) - - def create_pwls_dir(base_dir: str): - """Creates priority weighting layers subdirectory under BASE_DIR. - Skips creation of the subdirectory if it already exists. - """ - if not Path(base_dir).is_dir(): - return - - pwl_dir = f"{base_dir}/{PRIORITY_LAYERS_SEGMENT}" - message = tr( - "Missing parent directory when creating priority weighting layers subdirectory." - ) - FileUtils.create_new_dir(pwl_dir, message) - - @staticmethod - def create_new_dir(directory: str, log_message: str = ""): - """Creates new file directory if it doesn't exist""" - p = Path(directory) - if not p.exists(): - try: - p.mkdir() - except (FileNotFoundError, OSError): - log(log_message) - - @staticmethod - def create_new_file(file_path: str, log_message: str = ""): - """Creates new file""" - p = Path(file_path) - - if not p.exists(): - try: - p.touch(exist_ok=True) - except FileNotFoundError: - log(log_message) - - -def align_rasters( - input_raster_source, - reference_raster_source, - extent=None, - output_dir=None, - rescale_values=False, - resample_method=0, -): - """ - Based from work on https://github.com/inasafe/inasafe/pull/2070 - Aligns the passed raster files source and save the results into new files. - - :param input_raster_source: Input layer source - :type input_raster_source: str - - :param reference_raster_source: Reference layer source - :type reference_raster_source: str - - :param extent: Clip extent - :type extent: list - - :param output_dir: Absolute path of the output directory for the snapped - layers - :type output_dir: str - - :param rescale_values: Whether to rescale pixel values - :type rescale_values: bool - - :param resample_method: Method to use when resampling - :type resample_method: QgsAlignRaster.ResampleAlg - - """ - - try: - snap_directory = os.path.join(output_dir, "snap_layers") - - FileUtils.create_new_dir(snap_directory) - - input_path = Path(input_raster_source) - - input_layer_output = os.path.join( - f"{snap_directory}", f"{input_path.stem}_{str(uuid.uuid4())[:4]}.tif" - ) - - FileUtils.create_new_file(input_layer_output) - - align = QgsAlignRaster() - lst = [ - QgsAlignRaster.Item(input_raster_source, input_layer_output), - ] - - resample_method_value = QgsAlignRaster.ResampleAlg.RA_NearestNeighbour - - try: - resample_method_value = QgsAlignRaster.ResampleAlg(int(resample_method)) - except Exception as e: - log(f"Problem creating a resample value when snapping, {e}") - - if rescale_values: - lst[0].rescaleValues = rescale_values - - lst[0].resample_method = resample_method_value - - align.setRasters(lst) - align.setParametersFromRaster(reference_raster_source) - - layer = QgsRasterLayer(input_raster_source, "input_layer") - - extent = transform_extent( - layer.extent(), - QgsCoordinateReferenceSystem(layer.crs()), - QgsCoordinateReferenceSystem(align.destinationCrs()), - ) - - align.setClipExtent(extent) - - log(f"Snapping clip extent {layer.extent().asWktPolygon()} \n") - - if not align.run(): - log( - f"Problem during snapping for {input_raster_source} and " - f"{reference_raster_source}, {align.errorMessage()}" - ) - raise Exception(align.errorMessage()) - except Exception as e: - log( - f"Problem occured when snapping, {str(e)}." - f" Update snap settings and re-run the analysis" - ) - - return None, None - - log( - f"Finished snapping" - f" original layer - {input_raster_source}," - f"snapped output - {input_layer_output} \n" - ) - - return input_layer_output, None - - -class CustomJsonEncoder(json.JSONEncoder): - """ - Custom JSON encoder which handles UUID and datetime - """ - - def default(self, obj): - if isinstance(obj, UUID): - # if the obj is uuid, we simply return the value of uuid - return obj.hex - if isinstance(obj, datetime.datetime): - # if the obj is uuid, we simply return the value of uuid - return obj.isoformat() - return json.JSONEncoder.default(self, obj) - - -def todict(obj, classkey=None): - """ - Convert any object to dictionary - """ - - if isinstance(obj, dict): - data = {} - for k, v in obj.items(): - data[k] = todict(v, classkey) - return data - elif hasattr(obj, "_ast"): - return todict(obj._ast()) - elif hasattr(obj, "__iter__") and not isinstance(obj, str): - return [todict(v, classkey) for v in obj] - elif hasattr(obj, "__dict__"): - data = dict( - [ - (key, todict(value, classkey)) - for key, value in obj.__dict__.items() - if not callable(value) and not key.startswith("_") - ] - ) - if classkey is not None and hasattr(obj, "__class__"): - data[classkey] = obj.__class__.__name__ - return data - else: - return obj - - -def get_layer_type(file_path: str): - """ - Get layer type code from file path - """ - file_name, file_extension = os.path.splitext(file_path) - if file_extension.lower() in [".tif", ".tiff"]: - return 0 - elif file_extension.lower() in [".geojson", ".zip", ".shp"]: - return 1 - else: - return -1 diff --git a/django_project/cplus/views.py b/django_project/cplus/views.py deleted file mode 100644 index 60f00ef..0000000 --- a/django_project/cplus/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/django_project/cplus_api/tasks/runner.py b/django_project/cplus_api/tasks/runner.py index ce8c91f..d18e29b 100644 --- a/django_project/cplus_api/tasks/runner.py +++ b/django_project/cplus_api/tasks/runner.py @@ -14,10 +14,10 @@ def create_scenario_task_runner(scenario_task: ScenarioTask): # below imports require PyQGIS to be initialised from cplus_api.utils.worker_analysis import ( - TaskConfig, WorkerScenarioAnalysisTask + APITaskConfig, WorkerScenarioAnalysisTask ) - task_config = TaskConfig.from_dict(scenario_task.detail) + task_config = APITaskConfig.from_dict(scenario_task.detail) analysis_task = WorkerScenarioAnalysisTask(task_config, scenario_task) logger.info('Started prepare_run') analysis_task.prepare_run() diff --git a/django_project/cplus_api/utils/worker_analysis.py b/django_project/cplus_api/utils/worker_analysis.py index b7bffd5..5c6bf5c 100644 --- a/django_project/cplus_api/utils/worker_analysis.py +++ b/django_project/cplus_api/utils/worker_analysis.py @@ -10,15 +10,15 @@ from django.conf import settings from django.utils import timezone from django.template.loader import render_to_string -from cplus.models.base import ( +from cplus_core.models.base import ( Activity, NcsPathway, Scenario, SpatialExtent, LayerType ) -from cplus.tasks.analysis import ScenarioAnalysisTask -from cplus.utils.conf import Settings +from cplus_core.analysis import ScenarioAnalysisTask, TaskConfig +from cplus_core.utils.conf import Settings from cplus_api.models.scenario import ScenarioTask from cplus_api.models.layer import OutputLayer, InputLayer from cplus_api.utils.api_helper import ( @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -class TaskConfig(object): +class APITaskConfig(object): scenario_name = '' scenario_desc = '' @@ -197,7 +197,7 @@ def to_dict(self): @classmethod def from_dict(cls, data: dict) -> typing.Self: - config = TaskConfig( + config = APITaskConfig( data.get('scenario_name', ''), data.get('scenario_desc', ''), data.get('extent', []), [], [], [] ) @@ -342,25 +342,19 @@ def from_dict(cls, data: dict) -> typing.Self: return config -class WorkerScenarioAnalysisTask(ScenarioAnalysisTask): +class WorkerScenarioAnalysisTask(object): MIN_UPDATE_PROGRESS_IN_SECONDS = 1 - def __init__(self, task_config: TaskConfig, + def __init__(self, task_config: APITaskConfig, scenario_task: ScenarioTask): - super().__init__( - task_config.scenario_name, - task_config.scenario_desc, - task_config.analysis_activities, - task_config.priority_layer_groups, - task_config.analysis_extent, - task_config.scenario - ) self.task_config = task_config self.scenario_task = scenario_task self.last_update_progress = None self.downloaded_layers = {} self.downloaded_layer_count = 0 + self.scenario = task_config.scenario + self.analysis_task = None def prepare_run(self): # clear existing scenario directory if exists @@ -569,27 +563,11 @@ def transform_uuid_layer_paths(self, uuid_layers, layer_paths): uuid_mapped[uuid_str] = layer_paths[layer_uuid] return uuid_mapped - def get_settings_value(self, name: str, - default=None, setting_type=None): - return self.task_config.get_value(name, default) - - def get_scenario_directory(self): - return self.scenario_task.get_resources_path() - - def get_priority_layer(self, identifier): - return self.task_config.get_priority_layer(identifier) - - def get_activity(self, activity_uuid): - return self.task_config.get_activity(activity_uuid) - - def get_priority_layers(self): - return self.task_config.get_priority_layers() - - def cancel_task(self, exception=None): - self.error = exception + def cancel_task(self): + self.error = self.analysis_task.error if self.analysis_task else None # raise exception to stop the task - if exception: - raise exception + if self.error: + raise self.error else: raise Exception('Task is stopped with errors!') @@ -694,12 +672,12 @@ def upload_scenario_outputs(self): for group, files in scenario_output_files.items(): is_final_output = group == 'final_output' if is_final_output: - output_meta = self.output + output_meta = self.analysis_task.output if 'OUTPUT' in output_meta: del output_meta['OUTPUT'] self.create_and_upload_output_layer( files[0], self.scenario_task, - True, None, self.output + True, None, self.analysis_task.output ) total_uploaded_files += 1 self.set_custom_progress( @@ -801,6 +779,57 @@ def notify_user(self, is_success: bool): logger.error(exc) logger.error(traceback.format_exc()) + def run(self): + # create task_config object + analysis_config = TaskConfig( + self.scenario, + self.task_config.priority_layers, + self.scenario.priority_layer_groups, + self.task_config.analysis_activities, + self.task_config.get_value( + Settings.SNAPPING_ENABLED, default=False + ), + self.task_config.get_value(Settings.RESAMPLING_METHOD, default=0), + self.task_config.get_value( + Settings.RESCALE_VALUES, default=False + ), + self.task_config.get_value(Settings.PATHWAY_SUITABILITY_INDEX, default=0), + self.task_config.get_value(Settings.CARBON_COEFFICIENT, default=0.0), + self.task_config.get_value( + Settings.SIEVE_ENABLED, default=False + ), + self.task_config.get_value(Settings.SIEVE_THRESHOLD, default=10.0), + self.task_config.get_value( + Settings.NCS_WITH_CARBON, default=True + ), + self.task_config.get_value( + Settings.LANDUSE_PROJECT, default=True + ), + self.task_config.get_value( + Settings.LANDUSE_NORMALIZED, default=True + ), + self.task_config.get_value( + Settings.LANDUSE_WEIGHTED, default=True + ), + self.task_config.get_value( + Settings.HIGHEST_POSITION, default=True + ), + self.scenario_task.get_resources_path() + ) + + # create analysis task + self.analysis_task = ScenarioAnalysisTask(analysis_config) + + # setup signals + self.analysis_task.custom_progress_changed.connect(self.set_custom_progress) + self.analysis_task.status_message_changed.connect(self.set_status_message) + self.analysis_task.info_message_changed.connect(self.set_info_message) + self.analysis_task.log_received.connect(self.log_message) + self.analysis_task.task_cancelled.connect(self.cancel_task) + + # call run + self.analysis_task.run() + def finished(self, result: bool): if result: self.upload_scenario_outputs() From 50c8df43dead9247864a8a6a0cb462f3d8ba7cc4 Mon Sep 17 00:00:00 2001 From: Danang Massandy Date: Fri, 13 Sep 2024 05:46:45 +0700 Subject: [PATCH 2/5] fix lint and update docstring --- django_project/cplus_api/api_views/layer.py | 17 +- django_project/cplus_api/tasks/runner.py | 24 +- django_project/cplus_api/utils/api_helper.py | 98 +++++- django_project/cplus_api/utils/default.py | 2 + .../cplus_api/utils/worker_analysis.py | 278 +++++++++++++++++- 5 files changed, 403 insertions(+), 16 deletions(-) diff --git a/django_project/cplus_api/api_views/layer.py b/django_project/cplus_api/api_views/layer.py index 535f1ba..72582ad 100644 --- a/django_project/cplus_api/api_views/layer.py +++ b/django_project/cplus_api/api_views/layer.py @@ -41,7 +41,13 @@ def is_internal_user(user): - # check if user has internal user role + """Check if user has internal user role. + + :param user: user object + :type user: User + :return: True if user has internal role + :rtype: bool + """ user_profile = UserProfile.objects.filter( user=user ).first() @@ -53,6 +59,15 @@ def is_internal_user(user): def validate_layer_access(input_layer: InputLayer, user): + """Validate if user can access input layer. + + :param input_layer: input layer object + :type input_layer: InputLayer + :param user: user object + :type user: User + :return: True if user has permission to access the layer + :rtype: bool + """ if user.is_superuser: return True if input_layer.privacy_type == InputLayer.PrivacyTypes.COMMON: diff --git a/django_project/cplus_api/tasks/runner.py b/django_project/cplus_api/tasks/runner.py index d18e29b..77f14c2 100644 --- a/django_project/cplus_api/tasks/runner.py +++ b/django_project/cplus_api/tasks/runner.py @@ -12,6 +12,13 @@ def create_scenario_task_runner(scenario_task: ScenarioTask): + """Create and prepare worker task. + + :param scenario_task: scenario task object + :type scenario_task: ScenarioTask + :return: worker task that is ready to be run + :rtype: WorkerScenarioAnalysisTask + """ # below imports require PyQGIS to be initialised from cplus_api.utils.worker_analysis import ( APITaskConfig, WorkerScenarioAnalysisTask @@ -28,7 +35,13 @@ def create_scenario_task_runner(scenario_task: ScenarioTask): @shared_task(name="run_scenario_analysis_task") -def run_scenario_analysis_task(scenario_task_id): # pragma: no cover +def run_scenario_analysis_task(scenario_task_id): + """Run the scenario analysis. + + :param scenario_task_id: scenario task object id + :type scenario_task_id: int + """ + # Fetch ScenarioTask Object scenario_task = ScenarioTask.objects.get(id=scenario_task_id) scenario_task.task_on_started() scenario_task.code_version = ( @@ -37,6 +50,8 @@ def run_scenario_analysis_task(scenario_task_id): # pragma: no cover scenario_task.save(update_fields=['code_version']) logger.info( f'Triggered run_scenario_analysis_task {str(scenario_task.uuid)}') + + # initialize QGIS from qgis.core import QgsApplication # Supply path to qgis install location QgsApplication.setPrefixPath("/usr/bin/qgis", True) @@ -45,19 +60,22 @@ def run_scenario_analysis_task(scenario_task_id): # pragma: no cover qgs = QgsApplication([], False) # Load providers qgs.initQgis() + # init processing plugins import processing # noqa from processing.core.Processing import Processing Processing.initialize() + # Run scenario task analysis_task = create_scenario_task_runner(scenario_task) - start_time = time.time() analysis_task.run() logger.info(f'execution time: {time.time() - start_time} seconds') + # call finished() to upload layer outputs analysis_task.finished(True) + # use qgs.exit() if worker can be reused to execute another task qgs.exit() - # exitQgis causing worker lost + # NOTE: exitQgis causing worker lost # qgs.exitQgis() diff --git a/django_project/cplus_api/utils/api_helper.py b/django_project/cplus_api/utils/api_helper.py index 700a147..e2282db 100644 --- a/django_project/cplus_api/utils/api_helper.py +++ b/django_project/cplus_api/utils/api_helper.py @@ -52,8 +52,19 @@ class BaseScenarioReadAccess(object): """Base class to validate whether user can access the scenario.""" - def validate_user_access(self, user, scenario_task: ScenarioTask, - method='access'): + def validate_user_access( + self, user, scenario_task: ScenarioTask, method='access'): + """Validate user access when accessing a scenario task. + + :param user: Logged in User + :type user: User object + :param scenario_task: scenario task object + :type scenario_task: ScenarioTask + :param method: access type, defaults to 'access' + :type method: str, optional + :raises PermissionDenied: when user does not have no permission to + access the scenario task + """ if user.is_superuser: return if scenario_task.submitted_by != user: @@ -84,6 +95,13 @@ def get_page_size(request): def build_minio_absolute_url(url): + """Build minio absoulte URL only for Dev/DEBUG env. + + :param url: url + :type url: str + :return: url with absolute base url + :rtype: str + """ if not settings.DEBUG: return url minio_site = Site.objects.filter( @@ -98,6 +116,11 @@ def build_minio_absolute_url(url): def get_upload_client(): + """Get s3 client object to upload. + + :return: s3 client + :rtype: any + """ # Initialize upload client if settings.DEBUG: upload_client = boto3.client( @@ -115,6 +138,13 @@ def get_upload_client(): def get_presigned_url(filename): + """Generate presigned url of upload. + + :param filename: file name + :type filename: str + :return: presigned url + :rtype: str + """ upload_client = get_upload_client() bucket_name = os.environ.get("MINIO_BUCKET_NAME") try: @@ -138,6 +168,15 @@ def get_presigned_url(filename): def get_multipart_presigned_urls(filename, parts): + """Generate presigned urls for a file. + + :param filename: file name + :type filename: str + :param parts: number of parts that will be uploaded + :type parts: int + :return: Tuple of Multipart UploadId and presigned_urls + :rtype: tuple + """ upload_client = get_upload_client() bucket_name = os.environ.get("MINIO_BUCKET_NAME") response = upload_client.create_multipart_upload( @@ -167,8 +206,16 @@ def get_multipart_presigned_urls(filename, parts): def complete_multipart_upload(filename, upload_id, parts): - """ - Mark multipart upload as completed. + """Mark multipart upload as completed. + + :param filename: file name + :type filename: str + :param upload_id: Multipart UploadId + :type upload_id: str + :param parts: Dictionary of etag and part_number + :type parts: dict + :return: True + :rtype: bool """ upload_client = get_upload_client() bucket_name = os.environ.get("MINIO_BUCKET_NAME") @@ -190,7 +237,15 @@ def complete_multipart_upload(filename, upload_id, parts): def abort_multipart_upload(filename, upload_id): - """Abort multipart upload and return the part list.""" + """Abort multipart upload and return the part list. + + :param filename: file name + :type filename: str + :param upload_id: Multipart UploadId + :type upload_id: str + :return: total part number that has been uploaded + :rtype: int + """ upload_client = get_upload_client() bucket_name = os.environ.get("MINIO_BUCKET_NAME") parts = 0 @@ -220,6 +275,13 @@ def abort_multipart_upload(filename, upload_id): def convert_size(size_bytes): + """Convert byte size to humand readable text. + + :param size_bytes: byte sizse + :type size_bytes: int + :return: human readable text + :rtype: str + """ if size_bytes == 0: return "0B" size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") @@ -230,7 +292,16 @@ def convert_size(size_bytes): class CustomJsonEncoder(json.JSONEncoder): + """Class for custom object encoding.""" + def default(self, obj): + """Custom object encoding when converting to json. + + :param obj: object to be converted + :type obj: any + :return: json value + :rtype: any + """ if isinstance(obj, UUID): # if the obj is uuid, we simply return the value of uuid return str(obj) @@ -241,6 +312,15 @@ def default(self, obj): def todict(obj, classkey=None): + """Convert object to dictionary. + + :param obj: Object to be converted + :type obj: any + :param classkey: class definition, defaults to None + :type classkey: any, optional + :return: dictionary + :rtype: dict + """ if isinstance(obj, Enum): return obj.value elif isinstance(obj, dict): @@ -266,8 +346,12 @@ def todict(obj, classkey=None): def get_layer_type(file_path: str): - """ - Get layer type code from file path + """Get layer type code from file path. + + :param file_path: layer file path + :type file_path: str + :return: layer type + :rtype: int """ file_name, file_extension = os.path.splitext(file_path) if file_extension.lower() in ['.tif', '.tiff']: diff --git a/django_project/cplus_api/utils/default.py b/django_project/cplus_api/utils/default.py index ea171a4..2bced6d 100644 --- a/django_project/cplus_api/utils/default.py +++ b/django_project/cplus_api/utils/default.py @@ -1,5 +1,7 @@ class DEFAULT_VALUES(object): + """Class to store default values for configuration.""" + snapping_enabled = False pathway_suitability_index = 0 carbon_coefficient = 0.0 diff --git a/django_project/cplus_api/utils/worker_analysis.py b/django_project/cplus_api/utils/worker_analysis.py index 5c6bf5c..b8f09cf 100644 --- a/django_project/cplus_api/utils/worker_analysis.py +++ b/django_project/cplus_api/utils/worker_analysis.py @@ -33,6 +33,7 @@ class APITaskConfig(object): + """Class to parse the task config sent from API.""" scenario_name = '' scenario_desc = '' @@ -83,6 +84,62 @@ def __init__(self, scenario_name, scenario_desc, extent, landuse_normalized=DEFAULT_VALUES.landuse_normalized, landuse_weighted=DEFAULT_VALUES.landuse_weighted, highest_position=DEFAULT_VALUES.highest_position) -> None: + """Initialize APITaskConfig class. + + :param scenario_name: name of the scenario + :type scenario_name: str + :param scenario_desc: description of the scenario + :type scenario_desc: str + :param extent: scenario extent + :type extent: List[float] + :param analysis_activities: scenario activities + :type analysis_activities: List[Activity] + :param priority_layers: list of priority layer dict + :type priority_layers: List + :param priority_layer_groups: List of priority layer group dict + :type priority_layer_groups: List + :param snapping_enabled: enable snapping, defaults to False + :type snapping_enabled: bool, optional + :param snap_layer_uuid: Layer UUID of snap layer, defaults to '' + :type snap_layer_uuid: str, optional + :param pathway_suitability_index: Pathway suitability index, + defaults to DEFAULT_VALUES.pathway_suitability_index + :type pathway_suitability_index: int, optional + :param snap_rescale: Enable snap rescale, + defaults to DEFAULT_VALUES.snap_rescale + :type snap_rescale: bool, optional + :param snap_method: Snap method, + defaults to DEFAULT_VALUES.snap_method + :type snap_method: int, optional + :param sieve_enabled: Enable sieve function, + defaults to DEFAULT_VALUES.sieve_enabled + :type sieve_enabled: bool, optional + :param sieve_threshold: Sieve function threshold, + defaults to DEFAULT_VALUES.sieve_threshold + :type sieve_threshold: float, optional + :param sieve_mask_uuid: Layer UUID for sieve mask layer, + defaults to '' + :type sieve_mask_uuid: str, optional + :param mask_layer_uuids: Layer UUID for mask layer, defaults to '' + :type mask_layer_uuids: str, optional + :param scenario_uuid: UUID of a scenario, defaults to None + :type scenario_uuid: str, optional + :param ncs_with_carbon: Enable output ncs with carbon, + defaults to DEFAULT_VALUES.ncs_with_carbon + :type ncs_with_carbon: bool, optional + :param landuse_project: Enable output landuse project, + defaults to DEFAULT_VALUES.landuse_project + :type landuse_project: bool, optional + :param landuse_normalized: Enable output landuse normalized, + defaults to DEFAULT_VALUES.landuse_normalized + :type landuse_normalized: bool, optional + :param landuse_weighted: Enable output landuse weighted, + defaults to DEFAULT_VALUES.landuse_weighted + :type landuse_weighted: bool, optional + :param highest_position: Enable output highest position, + defaults to DEFAULT_VALUES.highest_position + :type highest_position: bool, optional + """ self.scenario_name = scenario_name self.scenario_desc = scenario_desc if scenario_uuid: @@ -120,6 +177,13 @@ def __init__(self, scenario_name, scenario_desc, extent, def get_activity( self, activity_uuid: str ) -> typing.Union[Activity, None]: + """Get activity object by its UUID. + + :param activity_uuid: activity UUID + :type activity_uuid: str + :return: Activity object or None if not found + :rtype: typing.Union[Activity, None] + """ activity = None filtered = [ act for act in self.analysis_activities if @@ -130,9 +194,21 @@ def get_activity( return activity def get_priority_layers(self) -> typing.List: + """Get all priority layers. + + :return: List of priority layer dictionary + :rtype: typing.List + """ return self.priority_layers def get_priority_layer(self, identifier) -> typing.Dict: + """Get priority layer dict by its UUID. + + :param identifier: Priority Layer UUID + :type identifier: str + :return: Priority Layer dict + :rtype: typing.Dict + """ priority_layer = None filtered = [ f for f in self.priority_layers if f['uuid'] == str(identifier)] @@ -141,9 +217,23 @@ def get_priority_layer(self, identifier) -> typing.Dict: return priority_layer def get_value(self, attr_name: Settings, default=None): + """Get attribute value by attribute name. + + :param attr_name: Attribute name/config key + :type attr_name: Settings + :param default: Default value if not found, defaults to None + :type default: any, optional + :return: Attribute value + :rtype: any + """ return getattr(self, attr_name.value, default) def to_dict(self): + """Convert API task config object to dictionary. + + :return: Dictionary of task config + :rtype: dict + """ input_dict = { 'scenario_name': self.scenario.name, 'scenario_desc': self.scenario.description, @@ -197,12 +287,21 @@ def to_dict(self): @classmethod def from_dict(cls, data: dict) -> typing.Self: + """Create APITaskConfig object from dictionary. + + :param data: dictionary from API + :type data: dict + :return: APITaskConfig + :rtype: APITaskConfig + """ config = APITaskConfig( data.get('scenario_name', ''), data.get('scenario_desc', ''), data.get('extent', []), [], [], [] ) config.priority_layers = data.get('priority_layers', []) config.priority_layer_groups = data.get('priority_layer_groups', []) + + # fetch analysis task configurations config.snapping_enabled = data.get( 'snapping_enabled', DEFAULT_VALUES.snapping_enabled) config.snap_layer_uuid = data.get('snap_layer_uuid', '') @@ -231,10 +330,13 @@ def from_dict(cls, data: dict) -> typing.Self: 'landuse_weighted', DEFAULT_VALUES.landuse_weighted) config.highest_position = data.get( 'highest_position', DEFAULT_VALUES.highest_position) + # store dict of config.priority_uuid_layers = {} config.pathway_uuid_layers = {} config.carbon_uuid_layers = {} + + # store priority layers for priority_layer in config.priority_layers: priority_layer_uuid = priority_layer.get('uuid', None) if not priority_layer_uuid: @@ -249,6 +351,8 @@ def from_dict(cls, data: dict) -> typing.Self: config.priority_uuid_layers[layer_uuid] = [ priority_layer_uuid ] + + # store activities _activities = data.get('activities', []) for activity in _activities: uuid_str = activity.get('uuid', None) @@ -272,6 +376,8 @@ def from_dict(cls, data: dict) -> typing.Self: config.priority_uuid_layers[m_priority_layer_uuid] = [ m_priority_uuid ] + + # create activity object activity_obj = Activity( uuid=uuid.UUID(uuid_str) if uuid_str else uuid.uuid4(), name=activity.get('name', ''), @@ -283,6 +389,8 @@ def from_dict(cls, data: dict) -> typing.Self: priority_layers=filtered_priority_layer, layer_styles=activity.get('layer_styles', {}) ) + + # create pathways pathways = activity.get('pathways', []) for pathway in pathways: pw_uuid_str = pathway.get('uuid', None) @@ -320,6 +428,8 @@ def from_dict(cls, data: dict) -> typing.Self: ] config.analysis_activities.append(activity_obj) + + # create scenario object config.scenario = Scenario( uuid=config.scenario_uuid, name=config.scenario_name, @@ -329,6 +439,8 @@ def from_dict(cls, data: dict) -> typing.Self: weighted_activities=[], priority_layer_groups=config.priority_layer_groups ) + + # calculate total input layers config.total_input_layers = ( len(config.pathway_uuid_layers) + len(config.priority_uuid_layers) + @@ -343,11 +455,19 @@ def from_dict(cls, data: dict) -> typing.Self: class WorkerScenarioAnalysisTask(object): + """Class to run scenario analysis in worker.""" MIN_UPDATE_PROGRESS_IN_SECONDS = 1 def __init__(self, task_config: APITaskConfig, scenario_task: ScenarioTask): + """Initialize WorkerScenarioAnalysisTask class. + + :param task_config: task config from API request + :type task_config: APITaskConfig + :param scenario_task: Task request object + :type scenario_task: ScenarioTask + """ self.task_config = task_config self.scenario_task = scenario_task self.last_update_progress = None @@ -357,19 +477,28 @@ def __init__(self, task_config: APITaskConfig, self.analysis_task = None def prepare_run(self): + """Prepare resources for the task.""" # clear existing scenario directory if exists self.scenario_task.clear_resources() + # create scenario directory for a user scenario_path = self.scenario_task.get_resources_path() os.makedirs(scenario_path) + # clear existing output results OutputLayer.objects.filter( scenario=self.scenario_task ).delete() + # download input layers self.initialize_input_layers(scenario_path) def initialize_input_layers(self, scenario_path: str): + """Initialize input layers required by analysis task. + + :param scenario_path: Base scenario directory + :type scenario_path: str + """ self.log_message( f'Initialize input layers: {self.task_config.total_input_layers}') self.set_custom_progress(0) @@ -385,6 +514,7 @@ def initialize_input_layers(self, scenario_path: str): scenario_path ) self.downloaded_layers.update(priority_layer_paths) + # init pathway layers pathway_layer_paths = {} pathway_uuids = self.task_config.pathway_uuid_layers.keys() @@ -423,8 +553,11 @@ def initialize_input_layers(self, scenario_path: str): if key in carbon_uuids }) + # Patch/Fix layer_path into priority layers dictionary if priority_layer_paths: self.patch_layer_path_to_priority_layers(priority_layer_paths) + + # Patch/Fix layer_path into activities self.patch_layer_path_to_activities( priority_layer_paths, pathway_layer_paths, @@ -485,6 +618,17 @@ def initialize_input_layers(self, scenario_path: str): def copy_input_layers_by_uuids( self, component_type: InputLayer.ComponentTypes, uuids: list, scenario_path: str): + """Download input layers by UUIDs to scenario directory. + + :param component_type: Layer component type + :type component_type: InputLayer.ComponentTypes + :param uuids: List of Layer UUID + :type uuids: list + :param scenario_path: scenario base directory + :type scenario_path: str + :return: Dictionary of Layer UUID and actual file path + :rtype: dict + """ results = {} layers = InputLayer.objects.filter( uuid__in=uuids @@ -514,6 +658,12 @@ def copy_input_layers_by_uuids( return results def patch_layer_path_to_priority_layers(self, priority_layer_paths): + """Patch/Fix layer_path into priority_layers dictionary. + + :param priority_layer_paths: Dictionary of Layer UUID and + actual file path + :type priority_layer_paths: dict + """ for priority_layer in self.task_config.priority_layers: layer_uuid = priority_layer.get('layer_uuid', None) if not layer_uuid or layer_uuid not in priority_layer_paths: @@ -523,6 +673,18 @@ def patch_layer_path_to_priority_layers(self, priority_layer_paths): def patch_layer_path_to_activities( self, priority_layer_paths, pathway_layer_paths, carbon_layer_paths): + """Patch/Fix layer_path into activities. + + :param priority_layer_paths: Dictionary of Layer UUID and + actual file path for priority layers + :type priority_layer_paths: dict + :param pathway_layer_paths: Dictionary of Layer UUID and + actual file path for ncs_pathways + :type pathway_layer_paths: dict + :param carbon_layer_paths: Dictionary of Layer UUID and + actual file path for carbon layers + :type carbon_layer_paths: dict + """ pw_uuid_mapped = self.transform_uuid_layer_paths( self.task_config.pathway_uuid_layers, pathway_layer_paths) priority_uuid_mapped = self.transform_uuid_layer_paths( @@ -549,12 +711,26 @@ def patch_layer_path_to_activities( carbon_paths.append( carbon_layer_paths[carbon_layer_uuid]) pathway.carbon_paths = carbon_paths + + # update reference object self.scenario.activities = self.task_config.analysis_activities self.analysis_activities = ( self.task_config.analysis_activities ) def transform_uuid_layer_paths(self, uuid_layers, layer_paths): + """Create mapping between Object UUID and layer file path. + + This is used to map the layer file path from Layer UUID back to + the actual objects that are using the layer. + + :param uuid_layers: Dictionary of Layer UUID and List of Object UUID + :type uuid_layers: dict + :param layer_paths: Dictionary of Layer UUID and actual file path + :type layer_paths: dict + :return: Dictionary of Objet UUID and layer file path + :rtype: dict + """ uuid_mapped = {} for layer_uuid, uuid_list in uuid_layers.items(): if layer_uuid not in layer_paths: @@ -564,6 +740,11 @@ def transform_uuid_layer_paths(self, uuid_layers, layer_paths): return uuid_mapped def cancel_task(self): + """Handle when task is cancelled. + + :raises self.error: Exception from analysis task if exists + :raises Exception: Default Exception + """ self.error = self.analysis_task.error if self.analysis_task else None # raise exception to stop the task if self.error: @@ -573,21 +754,51 @@ def cancel_task(self): def log_message(self, message: str, name: str = "qgis_cplus", info: bool = True, notify: bool = True): + """Handle when log is received from running task. + + :param message: Message log + :type message: str + :param name: log name, defaults to "qgis_cplus" + :type name: str, optional + :param info: True if it is information log, defaults to True + :type info: bool, optional + :param notify: Not used in API, defaults to True + :type notify: bool, optional + """ self.scenario_task.add_log( message, logging.INFO if info else logging.ERROR) level = logging.INFO if info else logging.WARNING logger.log(level, message) def set_status_message(self, message): + """Handle when status message is received from running task. + + :param message: Status/Progress Text Message + :type message: str + """ self.status_message = message self.scenario_task.progress_text = message self.scenario_task.save(update_fields=['progress_text']) def set_info_message(self, message, level): + """Handle when info message is received. + + :param message: Message log + :type message: str + :param level: severity level + :type level: int + """ # info_message seems the same with log_message self.info_message = message def set_custom_progress(self, value): + """Handle progress value to update task's progress. + + This method will limit the updating to database to + avoid too many queries. + :param value: Progress value + :type value: float + """ self.custom_progress = value self.scenario_task.progress = value # check how to control the frequency of updating progress @@ -598,6 +809,11 @@ def set_custom_progress(self, value): self.scenario_task.save(update_fields=['progress']) def should_update_progress(self): + """Check whether should update back to database. + + :return: True if last update time is more than 1s + :rtype: bool + """ if self.last_update_progress is None: return True ct = timezone.now() @@ -610,7 +826,24 @@ def create_and_upload_output_layer( self, file_path: str, scenario_task: ScenarioTask, is_final_output: bool, group: str, output_meta: dict = None) -> OutputLayer: + """Update output layer to object storage. + + :param file_path: output layer file path + :type file_path: str + :param scenario_task: ScenarioTask object + :type scenario_task: ScenarioTask + :param is_final_output: True if it is the final output layer + :type is_final_output: bool + :param group: layer group + :type group: str + :param output_meta: Metadata of layer, defaults to None + :type output_meta: dict, optional + :return: saved OutputLayer object + :rtype: OutputLayer + """ filename = os.path.basename(file_path) + + # convert to COG if it is Raster type if get_layer_type(file_path) == 0: cog_name = ( f"{os.path.basename(file_path).split('.')[0]}" @@ -644,6 +877,8 @@ def create_and_upload_output_layer( final_output_path = file_path else: final_output_path = file_path + + # create the OutputLayer object output_layer = OutputLayer.objects.create( name=filename, created_on=timezone.now(), @@ -655,11 +890,14 @@ def create_and_upload_output_layer( group=group, output_meta={} if not output_meta else output_meta ) + + # save the binary file to object storage with open(final_output_path, 'rb') as output_file: output_layer.file.save(filename, output_file) return output_layer def upload_scenario_outputs(self): + """Upload all scenario output layers to object storage.""" scenario_output_files, total_files = ( self.scenario_task.get_scenario_output_files() ) @@ -668,6 +906,8 @@ def upload_scenario_outputs(self): self.log_message(status_msg) self.log_message(json.dumps(scenario_output_files)) self.set_custom_progress(0) + + # iterate for each scenario output files total_uploaded_files = 0 for group, files in scenario_output_files.items(): is_final_output = group == 'final_output' @@ -691,6 +931,11 @@ def upload_scenario_outputs(self): 100 * (total_uploaded_files / total_files)) def notify_user(self, is_success: bool): + """Send email to notify user that analysis task is finished. + + :param is_success: True if task run successfully + :type is_success: bool + """ if not self.scenario_task.submitted_by.email: return try: @@ -733,6 +978,8 @@ def notify_user(self, is_success: bool): ), 'size': convert_size(layer.size) }) + + # render message in HTML string message = render_to_string( 'emails/analysis_completed.html', { @@ -760,6 +1007,8 @@ def notify_user(self, is_success: bool): 'errors': self.scenario_task.errors }, ) + + # send message subject = ( f'Your analysis of {scenario_name} ' 'has finished successfully' if @@ -780,6 +1029,7 @@ def notify_user(self, is_success: bool): logger.error(traceback.format_exc()) def run(self): + """Run the analysis task.""" # create task_config object analysis_config = TaskConfig( self.scenario, @@ -793,12 +1043,18 @@ def run(self): self.task_config.get_value( Settings.RESCALE_VALUES, default=False ), - self.task_config.get_value(Settings.PATHWAY_SUITABILITY_INDEX, default=0), - self.task_config.get_value(Settings.CARBON_COEFFICIENT, default=0.0), + self.task_config.get_value( + Settings.PATHWAY_SUITABILITY_INDEX, default=0 + ), + self.task_config.get_value( + Settings.CARBON_COEFFICIENT, default=0.0 + ), self.task_config.get_value( Settings.SIEVE_ENABLED, default=False ), - self.task_config.get_value(Settings.SIEVE_THRESHOLD, default=10.0), + self.task_config.get_value( + Settings.SIEVE_THRESHOLD, default=10.0 + ), self.task_config.get_value( Settings.NCS_WITH_CARBON, default=True ), @@ -821,8 +1077,10 @@ def run(self): self.analysis_task = ScenarioAnalysisTask(analysis_config) # setup signals - self.analysis_task.custom_progress_changed.connect(self.set_custom_progress) - self.analysis_task.status_message_changed.connect(self.set_status_message) + self.analysis_task.custom_progress_changed.connect( + self.set_custom_progress) + self.analysis_task.status_message_changed.connect( + self.set_status_message) self.analysis_task.info_message_changed.connect(self.set_info_message) self.analysis_task.log_received.connect(self.log_message) self.analysis_task.task_cancelled.connect(self.cancel_task) @@ -831,17 +1089,27 @@ def run(self): self.analysis_task.run() def finished(self, result: bool): + """Handle when task has been run. + + :param result: True if task run successfully + :type result: bool + """ if result: + # uplaod output files self.upload_scenario_outputs() else: self.log_message( f"Error from task scenario task {self.error}", info=False) + # clean directory self.scenario_task.clear_resources() + + # update scenario task object self.scenario_task.task_on_completed() self.scenario_task.updated_detail = json.loads( json.dumps(todict(self.scenario), cls=CustomJsonEncoder) ) self.scenario_task.save() + # send email to the submitter self.notify_user(result) From 2629de4aa13fa98443203b3b671c15d4af03e62d Mon Sep 17 00:00:00 2001 From: Danang Massandy Date: Fri, 13 Sep 2024 12:19:38 +0000 Subject: [PATCH 3/5] remove log --- django_project/cplus_api/tasks/runner.py | 6 ------ django_project/cplus_api/utils/worker_analysis.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/django_project/cplus_api/tasks/runner.py b/django_project/cplus_api/tasks/runner.py index 77f14c2..e647d22 100644 --- a/django_project/cplus_api/tasks/runner.py +++ b/django_project/cplus_api/tasks/runner.py @@ -3,9 +3,7 @@ from celery import shared_task import logging import time -import json from django.conf import settings -from core.settings.utils import UUIDEncoder from cplus_api.models.scenario import ScenarioTask logger = logging.getLogger(__name__) @@ -26,11 +24,7 @@ def create_scenario_task_runner(scenario_task: ScenarioTask): task_config = APITaskConfig.from_dict(scenario_task.detail) analysis_task = WorkerScenarioAnalysisTask(task_config, scenario_task) - logger.info('Started prepare_run') analysis_task.prepare_run() - logger.info('Finished prepare_run') - logger.info( - json.dumps(analysis_task.task_config.to_dict(), cls=UUIDEncoder)) return analysis_task diff --git a/django_project/cplus_api/utils/worker_analysis.py b/django_project/cplus_api/utils/worker_analysis.py index 0f6aedd..8a49fcb 100644 --- a/django_project/cplus_api/utils/worker_analysis.py +++ b/django_project/cplus_api/utils/worker_analysis.py @@ -1092,7 +1092,7 @@ def finished(self, result: bool): :type result: bool """ if result: - # uplaod output files + # upload output files self.upload_scenario_outputs() else: self.log_message( From 56c29b30e8134d58a28ecafbe5965a27e1d3dd70 Mon Sep 17 00:00:00 2001 From: Danang Massandy Date: Fri, 13 Sep 2024 13:25:17 +0000 Subject: [PATCH 4/5] fix activities in scenario --- django_project/cplus_api/utils/worker_analysis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_project/cplus_api/utils/worker_analysis.py b/django_project/cplus_api/utils/worker_analysis.py index 8a49fcb..c06c170 100644 --- a/django_project/cplus_api/utils/worker_analysis.py +++ b/django_project/cplus_api/utils/worker_analysis.py @@ -1032,6 +1032,7 @@ def run(self): self.scenario, self.task_config.priority_layers, self.scenario.priority_layer_groups, + self.scenario.activities, self.task_config.analysis_activities, self.task_config.get_value( Settings.SNAPPING_ENABLED, default=False From b8d48f709a42093bce555e341d1680ee7decedde Mon Sep 17 00:00:00 2001 From: Zulfikar Akbar Muzakki Date: Mon, 7 Oct 2024 19:50:11 +0700 Subject: [PATCH 5/5] Update CPLUS core version --- deployment/docker/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker/requirements.txt b/deployment/docker/requirements.txt index 67a4948..d1aaac2 100644 --- a/deployment/docker/requirements.txt +++ b/deployment/docker/requirements.txt @@ -63,4 +63,4 @@ django-revproxy @ https://github.com/jazzband/django-revproxy/archive/refs/tags/ rasterio==1.3.10 # cplus core -git+https://github.com/kartoza/cplus-core.git@feat-refactor-conf +git+https://github.com/kartoza/cplus-core.git@v0.0.4