Skip to content

Commit

Permalink
Merge pull request #590 from gkahiu/irrecoverable_carbon_metric
Browse files Browse the repository at this point in the history
Irrecoverable Carbon Metric
  • Loading branch information
Samweli committed Dec 18, 2024
2 parents 7b9f219 + 979f0e0 commit 756a37c
Show file tree
Hide file tree
Showing 34 changed files with 2,230 additions and 96 deletions.
11 changes: 8 additions & 3 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"general": {
"name" : "CPLUS plugin",
"name": "CPLUS plugin",
"qgisMinimumVersion": 3.22,
"qgisMaximumVersion": 3.99,
"icon": "icon.svg",
Expand All @@ -9,14 +9,19 @@
"homepage": "https://github.com/ConservationInternational/cplus-plugin",
"tracker": "https://github.com/ConservationInternational/cplus-plugin/issues",
"repository": "https://github.com/ConservationInternational/cplus-plugin",
"tags": ["cplus", "maps", "raster", "analytics"],
"tags": [
"cplus",
"maps",
"raster",
"analytics"
],
"category": "Plugins",
"hasProcessingProvider": "no",
"about": "Adds functionality to use the CPLUS decision support tool in making informed decisions, from spatial information such as land cover, carbon stocks, and potential for carbon sequestration, CPLUS enables the identification of key areas for intervention and investment.",
"author": "Kartoza",
"email": "info@kartoza.com",
"description": "QGIS plugin for the CPLUS framework",
"version": "0.0.1",
"version": "1.0.9dev",
"changelog": ""
}
}
21 changes: 21 additions & 0 deletions docs/developer/api/core/api_carbon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Conservation International
summary:
- Jeremy Prior
- Ketan Bamniya
date:
some_url:
copyright:
contact:
license: This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
---

# Carbon Calculation Utilities

::: src.cplus_plugin.lib.carbon
handler: python
options:
docstring_style: sphinx
heading_level: 1
show_source: true
show_root_heading: false
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ nav:
- Settings: developer/api/core/api_settings.md
- Utilities: developer/api/core/api_utils.md
- Financials: developer/api/core/api_financials.md
- Carbon: developer/api/core/api_carbon.md
- Reports:
- Comparison Table: developer/api/core/api_report_scenario_comparison_table.md
- Generator: developer/api/core/api_reports_generator.md
Expand Down
26 changes: 26 additions & 0 deletions src/cplus_plugin/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import concurrent.futures
import copy
import datetime
from enum import IntEnum
import json
import os

Expand Down Expand Up @@ -207,3 +208,28 @@ def fetch_scenario_output(
analysis_output=final_output,
)
return scenario, scenario_result


class ApiRequestStatus(IntEnum):
"""Status of API request."""

NOT_STARTED = 0
IN_PROGRESS = 1
COMPLETED = 2
ERROR = 3
CANCELED = 4

@staticmethod
def from_int(status: int) -> "ApiRequestStatus":
"""Gets the status from an int value.
:returns: The status from an int value.
:rtype: int
"""
return {
0: ApiRequestStatus.NOT_STARTED,
1: ApiRequestStatus.IN_PROGRESS,
2: ApiRequestStatus.COMPLETED,
3: ApiRequestStatus.ERROR,
4: ApiRequestStatus.CANCELED,
}[status]
297 changes: 297 additions & 0 deletions src/cplus_plugin/api/carbon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
"""
API requests for managing carbon layers.
"""

from datetime import datetime
import typing

from qgis.core import QgsApplication, QgsFileDownloader, QgsRectangle, QgsTask
from qgis.PyQt import QtCore

from .base import ApiRequestStatus
from ..conf import settings_manager, Settings
from ..models.helpers import extent_to_url_param
from ..utils import log, tr


class IrrecoverableCarbonDownloadTask(QgsTask):
"""Task for downloading the irrecoverable carbon dataset from the
online server.
The required information i.e. download URL, file path for saving
the downloaded file and extents for clipping the dataset are fetched
from the settings hence they need to be defined for the download
process to be successfully initiated.
"""

error_occurred = QtCore.pyqtSignal()
canceled = QtCore.pyqtSignal()
completed = QtCore.pyqtSignal()
started = QtCore.pyqtSignal()
exited = QtCore.pyqtSignal()

def __init__(self):
super().__init__(tr("Downloading irrecoverable carbon dataset"))
self._downloader = None
self._event_loop = None
self._errors = None
self._exited = False

@property
def errors(self) -> typing.List[str]:
"""Gets any errors encountered during the download process.
:returns: Download errors.
:rtype: typing.List[str]
"""
return [] if self._errors is None else self._errors

@property
def has_exited(self) -> bool:
"""Indicates whether the downloader has exited.
:returns: True if the downloader exited, else False.
:rtype: bool
"""
return self._exited

def cancel(self):
"""Cancel the download process."""
if self._downloader:
self._downloader.cancelDownload()
self._update_download_status(ApiRequestStatus.CANCELED, "Download canceled")
self.disconnect_receivers()

super().cancel()

if self._event_loop:
self._event_loop.quit()

log("Irrecoverable carbon dataset task canceled.")

def _on_error_occurred(self, error_messages: typing.List[str]):
"""Slot raised when the downloader encounters an error.
:param error_messages: Error messages.
:type error_messages: typing.List[str]
"""
self._errors = error_messages

err_msg = ", ".join(error_messages)
log(f"Error in downloading irrecoverable carbon dataset: {err_msg}", info=False)

self._update_download_status(
ApiRequestStatus.ERROR, tr("Download error. See logs for details.")
)

self._event_loop.quit()

self.error_occurred.emit()

def _on_download_canceled(self):
"""Slot raised when the download has been canceled."""
log("Download of irrecoverable carbon dataset canceled.")

self._event_loop.quit()

self.canceled.emit()

def _on_download_exited(self):
"""Slot raised when the download has exited."""
self._event_loop.quit()

self._exited = True

self.exited.emit()

def _on_download_completed(self, url: QtCore.QUrl):
"""Slot raised when the download is complete.
:param url: Url of the file resource.
:type url: QtCore.QUrl
"""
completion_datetime_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

log(
f"Download of irrecoverable carbon dataset successfully completed "
f"on {completion_datetime_str}."
)

self._update_download_status(
ApiRequestStatus.COMPLETED, tr("Download successful")
)

self._event_loop.quit()

self._successfully_completed = True

self.completed.emit()

def _on_progress_changed(self, received: int, total: int):
"""Slot raised indicating progress made by the downloader.
:param received: Bytes received.
:type received: int
:param total: Total size of the file in bytes.
:type total: int
"""
total_float = float(total)
if total_float == 0.0:
self.setProgress(total_float)
else:
self.setProgress(received / total_float * 100)

def _update_download_status(self, status: ApiRequestStatus, description: str):
"""Updates the settings with the online download status.
:param status: Download status to save.
:type status: ApiRequestStatus
:param description: Brief description of the status.
:type description: str
"""
settings_manager.set_value(
Settings.IRRECOVERABLE_CARBON_ONLINE_DOWNLOAD_STATUS, status.value
)
settings_manager.set_value(
Settings.IRRECOVERABLE_CARBON_ONLINE_STATUS_DESCRIPTION, description
)

def disconnect_receivers(self):
"""Disconnects all custom signals related to the downloader. This is
recommended prior to canceling the task.
"""
self._downloader.downloadError.disconnect(self._on_error_occurred)
self._downloader.downloadCanceled.disconnect(self._on_download_canceled)
self._downloader.downloadProgress.disconnect(self._on_progress_changed)
self._downloader.downloadCompleted.disconnect(self._on_download_completed)
self._downloader.downloadExited.disconnect(self._on_download_exited)

def run(self) -> bool:
"""Initiates the download of irrecoverable carbon dataset process and
returns a result indicating whether the process succeeded or failed.
:returns: True if the download process succeeded or False it if
failed.
:rtype: bool
"""
if self.isCanceled():
return False

# Get extents, URL and local path
extent = settings_manager.get_value(Settings.SCENARIO_EXTENT, default=None)
if extent is None:
log(
"Scenario extent not defined for downloading irrecoverable "
"carbon dataset.",
info=False,
)
return False

if len(extent) < 4:
log(
"Definition of scenario extent is incorrect. Consists of "
"less than 4 segments.",
info=False,
)
return False

extent_rectangle = QgsRectangle(
float(extent[0]), float(extent[2]), float(extent[1]), float(extent[3])
)
url_bbox_part = extent_to_url_param(extent_rectangle)
if not url_bbox_part:
log(
"Unable to create the bbox query part of the irrecoverable "
"carbon download URL.",
info=False,
)
return False

base_download_url_path = settings_manager.get_value(
Settings.IRRECOVERABLE_CARBON_ONLINE_SOURCE, default="", setting_type=str
)
if not base_download_url_path:
log("Source URL for irrecoverable carbon dataset not found.", info=False)
return False

full_download_url = QtCore.QUrl(base_download_url_path)
full_download_url.setQuery(url_bbox_part)

save_path = settings_manager.get_value(
Settings.IRRECOVERABLE_CARBON_ONLINE_LOCAL_PATH,
default="",
setting_type=str,
)
if not save_path:
log(
"Save location for irrecoverable carbon dataset not specified.",
info=False,
)
return False

# Use to block downloader until it completes or encounters an error
self._event_loop = QtCore.QEventLoop(self)

self._downloader = QgsFileDownloader(
full_download_url, save_path, delayStart=True
)
self._downloader.downloadError.connect(self._on_error_occurred)
self._downloader.downloadCanceled.connect(self._on_download_canceled)
self._downloader.downloadProgress.connect(self._on_progress_changed)
self._downloader.downloadCompleted.connect(self._on_download_completed)
self._downloader.downloadExited.connect(self._on_download_exited)

self._update_download_status(
ApiRequestStatus.NOT_STARTED, tr("Download not started")
)

self._downloader.startDownload()

self.started.emit()

self._update_download_status(
ApiRequestStatus.IN_PROGRESS, tr("Download ongoing")
)

log(
f"Started download of irrecoverable carbon dataset - {full_download_url.toString()} - "
f"on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)

self._event_loop.exec_()

return True


def get_downloader_task() -> typing.Optional[IrrecoverableCarbonDownloadTask]:
"""Gets the irrecoverable carbon task downloader in the QgsTaskManager.
:returns: The irrecoverable carbon task downloader in the QgsTaskManager
or None if not found.
:rtype: IrrecoverableCarbonDownloadTask
"""
ic_tasks = [
task
for task in QgsApplication.taskManager().tasks()
if isinstance(task, IrrecoverableCarbonDownloadTask)
]
if len(ic_tasks) == 0:
return None

return ic_tasks[0]


def start_irrecoverable_carbon_download():
"""Starts the process of downloading the reference irrecoverable carbon dataset.
Any ongoing downloading processing will be canceled.
"""
existing_download_task = get_downloader_task()
if existing_download_task:
existing_download_task.cancel()

new_download_task = IrrecoverableCarbonDownloadTask()
QgsApplication.taskManager().addTask(new_download_task)
Loading

0 comments on commit 756a37c

Please sign in to comment.