diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58eeaa7..f74f9f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,8 @@ jobs: fail-fast: false matrix: qgis_version_tag: - - release-3_16 + - release-3_26 + - release-3_30 - release-3_34 - release-3_36 os: [ubuntu-22.04] diff --git a/admin.py b/admin.py index ba25dc1..0c6f33e 100644 --- a/admin.py +++ b/admin.py @@ -4,6 +4,7 @@ """ import os +import sys import configparser import datetime as dt @@ -61,6 +62,20 @@ def main(context: typer.Context, verbose: bool = False, qgis_profile: str = "def } +def _qgis_profile_path() -> str: + """Returns the path segment to QGIS profiles folder based on the platform. + + :returns: Correct path segment corresponding to the current platform. + :rtype: str + """ + if sys.platform == "win32": + app_data_dir = "AppData/Roaming" + else: + app_data_dir = ".local/share" + + return f"{app_data_dir}/QGIS/QGIS3/profiles/" + + @app.command() def install(context: typer.Context, build_src: bool = True): """Deploys plugin to QGIS plugins directory @@ -80,8 +95,7 @@ def install(context: typer.Context, build_src: bool = True): ) root_directory = ( - Path.home() / f".local/share/QGIS/QGIS3/profiles/" - f"{context.obj['qgis_profile']}" + Path.home() / f"{_qgis_profile_path()}{context.obj['qgis_profile']}" ) base_target_directory = root_directory / "python/plugins" / SRC_NAME @@ -104,8 +118,7 @@ def symlink(context: typer.Context): build_path = LOCAL_ROOT_DIR / "build" / SRC_NAME root_directory = ( - Path.home() / f".local/share/QGIS/QGIS3/profiles/" - f"{context.obj['qgis_profile']}" + Path.home() / f"{_qgis_profile_path()}{context.obj['qgis_profile']}" ) destination_path = root_directory / "python/plugins" / SRC_NAME @@ -124,8 +137,7 @@ def uninstall(context: typer.Context): :type context: typer.Context """ root_directory = ( - Path.home() / f".local/share/QGIS/QGIS3/profiles/" - f"{context.obj['qgis_profile']}" + Path.home() / f"{_qgis_profile_path()}{context.obj['qgis_profile']}" ) base_target_directory = root_directory / "python/plugins" / SRC_NAME shutil.rmtree(str(base_target_directory), ignore_errors=True) @@ -136,6 +148,7 @@ def uninstall(context: typer.Context): def generate_zip( context: typer.Context, version: str = None, + file_name: str = None, output_directory: typing.Optional[Path] = LOCAL_ROOT_DIR / "dist", ): """Generates plugin zip folder, that can be used to installed the @@ -147,6 +160,9 @@ def generate_zip( :param version: Plugin version :type version: str + :param file_name: Plugin zip file name + :type file_name: str + :param output_directory: Directory where the zip folder will be saved. :type context: Path """ @@ -154,7 +170,10 @@ def generate_zip( metadata = _get_metadata() plugin_version = metadata["version"] if version is None else version output_directory.mkdir(parents=True, exist_ok=True) - zip_path = output_directory / f"{SRC_NAME}.{plugin_version}.zip" + zip_file_name = ( + f"{SRC_NAME}.{plugin_version}.zip" if file_name is None else file_name + ) + zip_path = output_directory / f"{zip_file_name}" with zipfile.ZipFile(zip_path, "w") as fh: _add_to_zip(build_dir, fh, arc_path_base=build_dir.parent) typer.echo( @@ -190,7 +209,7 @@ def build( :returns: Build directory path. :rtype: Path """ - if clean: + if clean and output_directory.exists(): shutil.rmtree(str(output_directory), ignore_errors=True) output_directory.mkdir(parents=True, exist_ok=True) copy_source_files(output_directory, tests=tests) @@ -201,7 +220,6 @@ def build( generate_metadata(context, output_directory) return output_directory - @app.command() def copy_icon( output_directory: typing.Optional[Path] = LOCAL_ROOT_DIR / "build/temp", @@ -247,14 +265,26 @@ def copy_source_files( for child in (LOCAL_ROOT_DIR / "src" / SRC_NAME).iterdir(): if child.name != "__pycache__": target_path = output_directory / child.name - handler = shutil.copytree if child.is_dir() else shutil.copy - handler(str(child.resolve()), str(target_path)) + if child.is_dir(): + shutil.copytree( + str(child.resolve()), + str(target_path), + ignore=shutil.ignore_patterns("*.pyc", "__pycache*"), + ) + else: + shutil.copy(str(child.resolve()), str(target_path)) if tests: for child in LOCAL_ROOT_DIR.iterdir(): if child.name in TEST_FILES: target_path = output_directory / child.name - handler = shutil.copytree if child.is_dir() else shutil.copy - handler(str(child.resolve()), str(target_path)) + if child.is_dir(): + shutil.copytree( + str(child.resolve()), + str(target_path), + ignore=shutil.ignore_patterns("*.pyc", "__pycache*"), + ) + else: + shutil.copy(str(child.resolve()), str(target_path)) @app.command() @@ -273,6 +303,12 @@ def compile_resources( resources_path = LOCAL_ROOT_DIR / "resources" / "resources.qrc" target_path = output_directory / "resources.py" target_path.parent.mkdir(parents=True, exist_ok=True) + + # Windows handling of paths for shlex.split function + if sys.platform == "win32": + target_path = target_path.as_posix() + resources_path = resources_path.as_posix() + _log(f"compile_resources target_path: {target_path}", context=context) subprocess.run(shlex.split(f"pyrcc5 -o {target_path} {resources_path}")) diff --git a/run-tests.sh b/run-tests.sh index d376a50..c589215 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -5,10 +5,12 @@ QGIS_IMAGE=qgis/qgis QGIS_IMAGE_latest=latest QGIS_IMAGE_V_3_26=release-3_26 -QGIS_VERSION_TAGS=($QGIS_IMAGE_latest $QGIS_IMAGE_V_3_26) +QGIS_VERSION_TAGS=($QGIS_IMAGE_V_3_26) export IMAGE=$QGIS_IMAGE +python admin.py build --tests + for TAG in "${QGIS_VERSION_TAGS[@]}" do echo "Running tests for QGIS $TAG" diff --git a/scripts/docker/qgis-gea-plugin-test-pre-scripts.sh b/scripts/docker/qgis-gea-plugin-test-pre-scripts.sh index 47cc2bc..25a87e9 100755 --- a/scripts/docker/qgis-gea-plugin-test-pre-scripts.sh +++ b/scripts/docker/qgis-gea-plugin-test-pre-scripts.sh @@ -2,7 +2,7 @@ qgis_setup.sh -# FIX default installation because the sources must be in "qgis-gea-plugin" parent folder -rm -rf /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis-gea-plugin -ln -sf /tests_directory /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis-gea-plugin -ln -sf /tests_directory /usr/share/qgis/python/plugins/qgis-gea-plugin +# FIX default installation because the sources must be in "qgis_gea_plugin" parent folder +rm -rf /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_gea_plugin +ln -sf /tests_directory /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_gea_plugin +ln -sf /tests_directory /usr/share/qgis/python/plugins/qgis_gea_plugin diff --git a/src/qgis_gea_plugin/conf.py b/src/qgis_gea_plugin/conf.py index e69de29..632ca66 100644 --- a/src/qgis_gea_plugin/conf.py +++ b/src/qgis_gea_plugin/conf.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +""" + Handles storage and retrieval of the plugin QgsSettings. +""" + +import contextlib +import dataclasses +import datetime +import enum +import json +import os.path +import uuid +from pathlib import Path + +from qgis.PyQt import QtCore +from qgis.core import QgsSettings + +from .utils import log + + +@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() + + +class Settings(enum.Enum): + """Plugin settings names""" + + HISTORICAL_VIEW = "historical" + NICFI_VIEW = "nicfi" + + +class SettingsManager(QtCore.QObject): + """Manages saving/loading settings for the plugin in QgsSettings.""" + + BASE_GROUP_NAME: str = "qgis_gea_plugin" + + 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}") + + +settings_manager = SettingsManager() diff --git a/src/qgis_gea_plugin/definitions/defaults.py b/src/qgis_gea_plugin/definitions/defaults.py new file mode 100644 index 0000000..c6193d9 --- /dev/null +++ b/src/qgis_gea_plugin/definitions/defaults.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +""" + Definitions for all defaults settings +""" + +PLUGIN_ICON = ":/plugins/qgis_gea_plugin/icon.png" + +ANIMATION_PLAY_ICON = ":/images/themes/default/mActionPlay.svg" +ANIMATION_PAUSE_ICON = ":/images/themes/default/temporal_navigation/pause.svg" \ No newline at end of file diff --git a/src/qgis_gea_plugin/gui/qgis_gea.py b/src/qgis_gea_plugin/gui/qgis_gea.py new file mode 100644 index 0000000..027af9e --- /dev/null +++ b/src/qgis_gea_plugin/gui/qgis_gea.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- + +""" +The plugin main window class file. +""" + +import os + +from qgis.PyQt import ( + QtCore, + QtGui, + QtWidgets, + QtNetwork, +) +from qgis.PyQt.uic import loadUiType + +from qgis.core import QgsProject, QgsInterval, QgsUnitTypes, QgsTemporalNavigationObject +from qgis.gui import QgsLayerTreeView + +from ..resources import * +from ..models.base import IMAGERY +from ..definitions.defaults import ANIMATION_PLAY_ICON, ANIMATION_PAUSE_ICON, PLUGIN_ICON +from ..conf import settings_manager, Settings +from ..utils import animation_state_change, log, tr + +WidgetUi, _ = loadUiType( + os.path.join(os.path.dirname(__file__), "../ui/main_dockwidget.ui") +) + + +class QgisGeaPlugin(QtWidgets.QDockWidget, WidgetUi): + """ + Main plugin UI class for QGIS GEA Plugin. + + This class represents the main dock widget for the plugin, providing + functionality for temporal navigation, layer management and plugin settings. + + """ + + def __init__(self, iface, parent=None): + """ + Initialize the QGIS Gea Plugin dock widget. + + :param iface: Reference to the QGIS interface. + :type iface: QgsInterface + + :param parent: Parent widget. Defaults to None. + :type parent: QWidget + + """ + super().__init__(parent) + self.setupUi(self) + self.iface = iface + + self.navigation_object = QgsTemporalNavigationObject(self) + self.navigation_object.setFrameDuration( + QgsInterval(1, QgsUnitTypes.TemporalIrregularStep) + ) + + self.current_imagery_type = IMAGERY.HISTORICAL + + icon_pixmap = QtGui.QPixmap(PLUGIN_ICON) + self.icon_la.setPixmap(icon_pixmap) + + self.play_btn.setIcon(QtGui.QIcon(ANIMATION_PLAY_ICON)) + + self.time_values = [] + + self.historical_imagery.setChecked( + settings_manager.get_value( + Settings.HISTORICAL_VIEW, + setting_type=bool, + default=True) + ) + + self.nicfi_imagery.setChecked( + settings_manager.get_value( + Settings.NICFI_VIEW, + setting_type=bool, + default=False) + ) + self.prepare_time_slider() + + self.historical_imagery.toggled.connect(self.prepare_time_slider) + self.nicfi_imagery.toggled.connect(self.prepare_time_slider) + + self.play_btn.clicked.connect(self.animate_layers) + self.navigation_object.updateTemporalRange.connect( + self.temporal_range_changed + ) + self.time_slider.valueChanged.connect( + self.slider_value_changed + ) + + self.iface.projectRead.connect(self.prepare_time_slider) + + def slider_value_changed(self, value): + """ + Slot function for handling time slider value change. + + :param value: New value of the slider. + :type value: int + """ + self.navigation_object.setCurrentFrameNumber(value) + + def animate_layers(self): + """ + Toggle animation of layers based on the current animation state. + This function is called when user press the play button. + """ + if self.navigation_object.animationState() == \ + QgsTemporalNavigationObject.AnimationState.Idle: + self.play_btn.setIcon(QtGui.QIcon(ANIMATION_PAUSE_ICON)) + self.play_btn.setToolTip(tr("Pause animation")) + self.navigation_object.playForward() + else: + self.navigation_object.pause() + self.play_btn.setToolTip(tr("Click to play animation")) + self.play_btn.setIcon(QtGui.QIcon(ANIMATION_PLAY_ICON)) + + def temporal_range_changed(self, temporal_range): + """ + Update temporal range and UI elements when temporal range changes. + + :param temporal_range: New temporal range. + :type temporal_range: QgsDateTimeRange + """ + self.iface.mapCanvas().setTemporalRange(temporal_range) + self.temporal_range_la.setText( + tr( + f'Current time range: ' + f'{temporal_range.begin().toString("yyyy-MM-ddTHH:mm:ss")} to ' + f'{temporal_range.end().toString("yyyy-MM-ddTHH:mm:ss")} ' + )) + self.time_slider.setValue( + self.navigation_object.currentFrameNumber() + ) + + # On the last animation frame + if self.navigation_object.currentFrameNumber() == \ + len(self.navigation_object.availableTemporalRanges()) - 1: + + self.play_btn.setIcon(QtGui.QIcon(ANIMATION_PLAY_ICON)) + + def prepare_time_slider(self): + """ + Prepare the time slider based on current selected imagery type. + """ + values = [] + set_layer = None + active_layer = None + + closed_imagery = None + + if self.historical_imagery.isChecked(): + settings_manager.set_value(Settings.HISTORICAL_VIEW, True) + settings_manager.set_value(Settings.NICFI_VIEW, False) + + self.current_imagery_type = IMAGERY.HISTORICAL + closed_imagery = IMAGERY.NICFI + else: + settings_manager.set_value(Settings.NICFI_VIEW, True) + settings_manager.set_value(Settings.HISTORICAL_VIEW, False) + + self.current_imagery_type = IMAGERY.NICFI + closed_imagery = IMAGERY.HISTORICAL + + layers = QgsProject.instance().mapLayers() + for path, layer in layers.items(): + if layer.metadata().contains( + self.current_imagery_type.value.lower() + ): + values.append( + layer.temporalProperties().fixedTemporalRange() + ) + active_layer = layer + elif layer.metadata().contains( + closed_imagery.value.lower() + ): + set_layer = layer + + self.update_layer_group(set_layer) + self.update_layer_group(active_layer, True) + + self.time_slider.setRange(0, len(values) - 1) + self.navigation_object.setAvailableTemporalRanges(values) + + temporal_range = values[0] if len(values) > 0 else None + + if temporal_range: + self.iface.mapCanvas().setTemporalRange(temporal_range) + self.temporal_range_la.setText( + tr( + f'Current time range: ' + f'{temporal_range.begin().toString("yyyy-MM-ddTHH:mm:ss")} to ' + f'{temporal_range.end().toString("yyyy-MM-ddTHH:mm:ss")} ' + )) + + def update_layer_group(self, layer, show=False): + """ + Update visibility of provided layer parent group. + + :param layer: Layer to update. + :type layer: QgsMapLayer + + :param show: Group visibility state. Defaults to False. + :type show: bool + """ + if layer is not None: + root = QgsProject.instance().layerTreeRoot() + layer_tree = root.findLayer(layer.id()) + + if layer_tree is not None: + group_tree = layer_tree.parent() + if group_tree is not None: + group_tree.setItemVisibilityCheckedRecursive(show) diff --git a/src/qgis_gea_plugin/gui/qgis_gea_plugin.py b/src/qgis_gea_plugin/gui/qgis_gea_plugin.py deleted file mode 100644 index dd8ee32..0000000 --- a/src/qgis_gea_plugin/gui/qgis_gea_plugin.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - The plugin main window class file -""" - -import os - -from qgis.PyQt import ( - QtCore, - QtGui, - QtWidgets, - QtNetwork, -) -from qgis.PyQt.uic import loadUiType - -from ..resources import * - - -WidgetUi, _ = loadUiType( - os.path.join(os.path.dirname(__file__), "../ui/main_dockwidget.ui") -) - - -class QgisGeaPlugin(QtWidgets.QDockWidget, WidgetUi): - """Main plugin UI""" - - def __init__( - self, - iface, - parent=None, - ): - super().__init__(parent) - self.setupUi(self) - self.iface = iface - - icon_pixmap = QtGui.QPixmap(":/plugins/qgis_gea_plugin/icon.png") - self.icon_la.setPixmap(icon_pixmap) - - self.play_btn.setIcon( - QtGui.QIcon(":/images/themes/default/mActionPlay.svg") - ) - diff --git a/src/qgis_gea_plugin/main.py b/src/qgis_gea_plugin/main.py index 117494c..287ac01 100644 --- a/src/qgis_gea_plugin/main.py +++ b/src/qgis_gea_plugin/main.py @@ -19,7 +19,7 @@ # Initialize Qt resources from file resources.py from .resources import * -from .gui.qgis_gea_plugin import QgisGeaPlugin +from .gui.qgis_gea import QgisGeaPlugin class QgisGea: diff --git a/src/qgis_gea_plugin/models/base.py b/src/qgis_gea_plugin/models/base.py new file mode 100644 index 0000000..cb486b2 --- /dev/null +++ b/src/qgis_gea_plugin/models/base.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +""" Plugin models. +""" + +import dataclasses +import datetime +from enum import Enum, IntEnum + +class IMAGERY(Enum): + """Project imagery types""" + + HISTORICAL = "Historical" + NICFI = "Nicfi" \ No newline at end of file diff --git a/src/qgis_gea_plugin/ui/main_dockwidget.ui b/src/qgis_gea_plugin/ui/main_dockwidget.ui index 0f0f65d..9a8fe3a 100644 --- a/src/qgis_gea_plugin/ui/main_dockwidget.ui +++ b/src/qgis_gea_plugin/ui/main_dockwidget.ui @@ -7,7 +7,7 @@ 0 0 725 - 588 + 610 @@ -118,7 +118,7 @@ - + 10 @@ -126,31 +126,50 @@ Qt::Horizontal - QSlider::TicksBothSides + QSlider::TicksBelow - + Current Imagery (NICIFI) + + buttonGroup + - + Historical Imagery (Landsat) + + true + + + buttonGroup + + + Play animation + ... + + + + + + + @@ -291,4 +310,7 @@ + + + diff --git a/src/qgis_gea_plugin/utils.py b/src/qgis_gea_plugin/utils.py index e69de29..2bd23f4 100644 --- a/src/qgis_gea_plugin/utils.py +++ b/src/qgis_gea_plugin/utils.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" + Plugin utilities +""" + +import hashlib +import json +import os +import uuid +import datetime +from pathlib import Path + +from qgis.PyQt import QtCore, QtGui +from qgis.core import ( + Qgis, + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsCoordinateTransformContext, + QgsDistanceArea, + QgsMessageLog, + QgsProcessingFeedback, + QgsProject, + QgsProcessing, + QgsRasterLayer, + QgsRectangle, + QgsUnitTypes, +) + + + + +def log( + message: str, + name: str = "qgis_gea", + 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 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("QgisGea", message) + + +def animation_state_change(value): + log(f"{value}") + pass + diff --git a/test/test_main_dock.py b/test/test_main_dock.py new file mode 100644 index 0000000..689a1f9 --- /dev/null +++ b/test/test_main_dock.py @@ -0,0 +1,114 @@ +import unittest +from qgis.PyQt.QtCore import Qt, QDateTime +from qgis.PyQt.QtTest import QTest + +from qgis.core import QgsDateTimeRange, QgsInterval, QgsUnitTypes, QgsTemporalNavigationObject +from qgis.gui import QgsMapCanvas + +from qgis.utils import plugins, iface + +from qgis_gea_plugin.gui.qgis_gea import QgisGeaPlugin + +from qgis_gea_plugin.models.base import IMAGERY + +from utilities_for_testing import get_qgis_app + +QGIS_APP, CANVAS, IFACE, PARENT = get_qgis_app() + +class TestQgisGeaPlugin(unittest.TestCase): + """ + Unit tests for QgisGeaPlugin class. + """ + + @classmethod + def setUpClass(cls): + """ + Set up the QGIS GEA Plugin dock instance for testing. + """ + cls.plugin_dock = QgisGeaPlugin(iface) + iface.mainWindow().addDockWidget( + Qt.RightDockWidgetArea, + cls.plugin_dock + ) + + def test_slider_value_changed(self): + """ + Test slider value change functionality. + """ + + ranges = [ + QgsDateTimeRange( + QDateTime.fromString("2020-01-01T00:00:00", Qt.ISODate), + QDateTime.fromString("2020-01-01T00:00:00", Qt.ISODate)), + QgsDateTimeRange( + QDateTime.fromString("2020-01-01T00:00:00", Qt.ISODate), + QDateTime.fromString("2020-01-01T00:00:00", Qt.ISODate)) + ] + self.plugin_dock.navigation_object.setFrameDuration( + QgsInterval(1, QgsUnitTypes.TemporalIrregularStep) + ) + self.plugin_dock.navigation_object.setAvailableTemporalRanges(ranges) + + self.assertEqual(self.plugin_dock.navigation_object.currentFrameNumber(), 0) + + # Simulate setting slider value + slider_value = 1 + self.plugin_dock.slider_value_changed(slider_value) + + # Check if navigation object's current frame number is set correctly + self.assertEqual( + self.plugin_dock.navigation_object.currentFrameNumber(), + slider_value + ) + + def test_animate_layers(self): + """ + Test the main animation function. + """ + self.assertEqual( + self.plugin_dock.navigation_object.animationState(), + QgsTemporalNavigationObject.AnimationState.Idle + ) + # Simulate clicking on the play button + QTest.mouseClick(self.plugin_dock.play_btn, Qt.LeftButton) + + self.assertEqual( + self.plugin_dock.navigation_object.animationState(), + QgsTemporalNavigationObject.AnimationState.Forward + ) + + # Simulate clicking on the play button again + QTest.mouseClick(self.plugin_dock.play_btn, Qt.LeftButton) + + self.assertEqual( + self.plugin_dock.navigation_object.animationState(), + QgsTemporalNavigationObject.AnimationState.Idle + ) + + def test_prepare_time_slider(self): + """ + Test preparation of time slider UI components. + """ + self.plugin_dock.historical_imagery.setChecked(True) + self.plugin_dock.prepare_time_slider() + + # Check if the current imagery type is set correctly + self.assertEqual(self.plugin_dock.current_imagery_type, IMAGERY.HISTORICAL) + + self.plugin_dock.nicfi_imagery.setChecked(True) + self.plugin_dock.prepare_time_slider() + + # Check if the current imagery type is set correctly + self.assertEqual(self.plugin_dock.current_imagery_type, IMAGERY.NICFI) + + @classmethod + def tearDownClass(cls): + """ + Clean up after tests. + """ + # Remove the dock widget after tests + iface.mainWindow().removeDockWidget(cls.plugin_dock) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_suite.py b/test_suite.py index a2b7876..25f8a71 100644 --- a/test_suite.py +++ b/test_suite.py @@ -11,7 +11,7 @@ try: import coverage except ImportError: - pipmain(['install', 'coverage']) + pipmain(["install", "coverage"]) import coverage import tempfile from osgeo import gdal @@ -27,17 +27,17 @@ def _run_tests(test_suite, package_name, with_coverage=False): version = str(Qgis.QGIS_VERSION_INT) version = int(version) - print('########') - print('%s tests has been discovered in %s' % (count, package_name)) - print('QGIS : %s' % version) - print('Python GDAL : %s' % gdal.VersionInfo('VERSION_NUM')) - print('QT : %s' % Qt.QT_VERSION_STR) - print('Run slow tests : %s' % (not os.environ.get('ON_TRAVIS', False))) - print('########') + print("########") + print("%s tests has been discovered in %s" % (count, package_name)) + print("QGIS : %s" % version) + print("Python GDAL : %s" % gdal.VersionInfo("VERSION_NUM")) + print("QT : %s" % Qt.QT_VERSION_STR) + print("Run slow tests : %s" % (not os.environ.get("ON_TRAVIS", False))) + print("########") if with_coverage: cov = coverage.Coverage( - source=['./'], - omit=['*/test/*', './definitions/*'], + source=["./"], + omit=["*/test/*", "./definitions/*"], ) cov.start() @@ -48,14 +48,14 @@ def _run_tests(test_suite, package_name, with_coverage=False): cov.save() report = tempfile.NamedTemporaryFile(delete=False) cov.report(file=report) - # Produce HTML reports in the `htmlcov` folder and open index.html + # Produce HTML report_templates in the `htmlcov` folder and open index.html # cov.html_report() report.close() - with open(report.name, 'r') as fin: + with open(report.name, "r") as fin: print(fin.read()) -def test_package(package='test'): +def test_package(package="test"): """Test package. This function is called by Github actions or travis without arguments. :param package: The package to test.