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.