Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add image gui #229

Draft
wants to merge 7 commits into
base: feature/menu-buildout
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/creating_ui_components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# General steps in building a new UI component for the EarthEngine Plugin (Notes)

New UI components should have unit tests written to cover their functionality, see [this example](../test/test_form_add_ee_image.py)

Once you have defined your test cases, the general procedure for leveraging the UI helpers functions are as follows:
1. Create a callback function that will receive a dictionnary with the form parameters from the Qt dialog (see: [_load_gee_layer](../ee_plugin/ui/form_add_ee_image.py))
2. Create a Qt Dialog using the helpers from the [UI utilities](../ee_plugin/ui/utils.py), see: [add_gee_layer_dialog](../ee_plugin/ui/form_add_ee_image.py).
3. Create a new action in the the EarthEngine plugin menu which opens the dialog, see: [plugin actions](../ee_plugin/ee_plugin.py)

10 changes: 10 additions & 0 deletions ee_plugin/ee_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from qgis.PyQt.QtGui import QIcon

from .config import EarthEngineConfig
from .ui.form_add_ee_image import add_gee_layer_dialog
from .ui.utils import (
build_form_group_box,
build_vbox_dialog,
Expand Down Expand Up @@ -118,6 +119,13 @@ def initGui(self):
triggered=self._run_cmd_set_cloud_project,
)

add_gee_layer_action = QtWidgets.QAction(
icon=icon("add-layer.svg"),
text=self.tr("Add GEE Image"),
parent=self.iface.mainWindow(),
triggered=add_gee_layer_dialog,
)

# Build plugin menu
plugin_menu = cast(QtWidgets.QMenu, self.iface.pluginMenu())
ee_menu = plugin_menu.addMenu(
Expand All @@ -129,6 +137,7 @@ def initGui(self):
ee_menu.addSeparator()
ee_menu.addAction(sign_in_action)
ee_menu.addAction(self.set_cloud_project_action)
ee_menu.addAction(add_gee_layer_action)

# Build toolbar
toolButton = QtWidgets.QToolButton()
Expand All @@ -145,6 +154,7 @@ def initGui(self):
toolButton.menu().addSeparator()
toolButton.menu().addAction(sign_in_action)
toolButton.menu().addAction(self.set_cloud_project_action)
toolButton.menu().addAction(add_gee_layer_action)
self.iface.pluginToolBar().addWidget(toolButton)
self.toolButton = toolButton

Expand Down
110 changes: 110 additions & 0 deletions ee_plugin/ui/form_add_ee_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json
from typing import Dict

import ee
from qgis import gui
from qgis.PyQt import QtWidgets
from qgis.core import QgsMessageLog, Qgis

from ..Map import addLayer
from .utils import (
build_form_group_box,
build_vbox_dialog,
get_values,
)


def add_gee_layer_dialog(iface: gui.QgisInterface) -> QtWidgets.QDialog:
"""Display a dialog to add a GEE dataset to the QGIS map."""

dialog = build_vbox_dialog(
windowTitle="Add Google Earth Engine Image",
widgets=[
build_form_group_box(
title="Dataset",
rows=[
(
QtWidgets.QLabel(
text="Enter GEE Image Name (e.g., COPERNICUS/S2, USGS/SRTMGL1_003)",
toolTip="Provide the full Earth Engine ID.",
),
QtWidgets.QLineEdit(objectName="imageId"),
)
],
),
build_form_group_box(
title="Visualization Parameters (JSON)",
collapsable=True,
collapsed=True,
rows=[
(
QtWidgets.QLabel(
text="Enter JSON for visualization parameters",
toolTip="Example: {'min': 0, 'max': 4000, 'palette': ['006633', 'E5FFCC', '662A00']}",
),
QtWidgets.QTextEdit(
objectName="vizParams",
placeholderText='{"min": 0, "max": 4000, "palette": ["006633", "E5FFCC", "662A00"]}',
),
)
],
),
],
accepted=lambda: _load_gee_layer(dialog),
rejected=lambda: QgsMessageLog.logMessage(
"User cancelled adding GEE Layer.", "GEE Plugin", level=Qgis.Info
),
)

return dialog


def _load_gee_layer(dialog: Dict[str, QtWidgets.QWidget]):
"""Fetch and add the selected Earth Engine dataset to the map with user-defined visualization parameters."""
values = get_values(dialog)
image_id = values["imageId"]

if not image_id:
message = "Image ID is required."
QgsMessageLog.logMessage(message, "GEE Plugin", level=Qgis.Critical)
return

try:
# Get asset metadata
asset_info = ee.data.getAsset(image_id)
asset_type = asset_info.get("type", "")

if asset_type == "IMAGE":
ee_object = ee.Image(image_id)
else:
raise ValueError(f"Unsupported asset type: {asset_type}")

# Get visualization parameters from user as JSON
vis_params_input = values.get("vizParams", "{}")
if not vis_params_input:
vis_params = {}
else:
try:
vis_params = json.loads(vis_params_input.replace("'", '"'))
except json.JSONDecodeError:
raise ValueError("Invalid JSON format in visualization parameters.")

# Add the dataset to QGIS map
addLayer(ee_object, vis_params, image_id)

success_message = (
f"Successfully added {image_id} to the map with custom visualization."
)
QgsMessageLog.logMessage(success_message, "GEE Plugin", level=Qgis.Success)

except ee.EEException as e:
error_message = f"Earth Engine Error: {str(e)}"
QgsMessageLog.logMessage(error_message, "GEE Plugin", level=Qgis.Critical)

except ValueError as e:
error_message = str(e)
QgsMessageLog.logMessage(error_message, "GEE Plugin", level=Qgis.Critical)

except Exception as e:
error_message = f"Unexpected error: {str(e)}"
QgsMessageLog.logMessage(error_message, "GEE Plugin", level=Qgis.Critical)
2 changes: 2 additions & 0 deletions ee_plugin/ui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
QDateEdit,
QCheckBox,
QLayout,
QTextEdit,
)
from qgis.gui import QgsColorButton, QgsCollapsibleGroupBox

Expand Down Expand Up @@ -82,6 +83,7 @@ def get_values(dialog: QDialog) -> dict:
QDateEdit: lambda w: w.date().toString("yyyy-MM-dd"),
QCheckBox: lambda w: w.isChecked(),
QgsColorButton: lambda w: w.color().name(),
QTextEdit: lambda w: w.toPlainText().strip(),
}
return {
w.objectName(): parsers[type(w)](w)
Expand Down
7 changes: 7 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ def load_ee_plugin(qgis_app, setup_ee, ee_config):
plugins["ee_plugin"] = plugin
plugin.check_version()
yield qgis_app


@fixture(scope="function", autouse=True)
def qgis_iface_clean(qgis_iface):
"""Remove all layers from the map canvas after each test."""
qgis_iface.mapCanvas().setLayers([])
yield qgis_iface
81 changes: 81 additions & 0 deletions test/test_form_add_ee_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from qgis.PyQt import QtWidgets

from ee_plugin.ui.utils import get_values
from ee_plugin.ui.form_add_ee_image import add_gee_layer_dialog, _load_gee_layer


def test_add_gee_layer_dialog(qgis_iface_clean):
dialog = add_gee_layer_dialog(qgis_iface_clean)
dialog.findChild(QtWidgets.QLineEdit, "imageId").setText("COPERNICUS/S2")

dialog.findChild(QtWidgets.QTextEdit, "vizParams").setText(
'{"min": 0, "max": 4000, "palette": ["006633", "E5FFCC", "662A00"]}'
)

assert get_values(dialog) == {
"imageId": "COPERNICUS/S2",
"vizParams": '{"min": 0, "max": 4000, "palette": ["006633", "E5FFCC", "662A00"]}',
}


def test_load_gee_layer_srtm(qgis_iface_clean):
dialog = add_gee_layer_dialog(qgis_iface_clean)
dialog.findChild(QtWidgets.QLineEdit, "imageId").setText("USGS/SRTMGL1_003")

dialog.findChild(QtWidgets.QTextEdit, "vizParams").setText(
'{"min": 0, "max": 4000, "palette": ["006633", "E5FFCC", "662A00"]}'
)

assert get_values(dialog) == {
"imageId": "USGS/SRTMGL1_003",
"vizParams": '{"min": 0, "max": 4000, "palette": ["006633", "E5FFCC", "662A00"]}',
}

_load_gee_layer(dialog)

assert len(qgis_iface_clean.mapCanvas().layers()) == 1
assert qgis_iface_clean.mapCanvas().layers()[0].name() == "USGS/SRTMGL1_003"
assert qgis_iface_clean.mapCanvas().layers()[0].dataProvider().name() == "EE"


def test_converting_viz_params_json(qgis_iface_clean):
dialog = add_gee_layer_dialog(qgis_iface_clean)
dialog.findChild(QtWidgets.QLineEdit, "imageId").setText("USGS/SRTMGL1_003")

# single quotes should get replaced to double quotes
# by _load_gee_layer, so dialog still has single quotes
dialog.findChild(QtWidgets.QTextEdit, "vizParams").setText(
"{'min': 0, 'max': 4000, 'palette': ['006633', 'E5FFCC', '662A00']}"
)

_load_gee_layer(dialog)

assert len(qgis_iface_clean.mapCanvas().layers()) == 1
assert qgis_iface_clean.mapCanvas().layers()[0].name() == "USGS/SRTMGL1_003"
assert qgis_iface_clean.mapCanvas().layers()[0].dataProvider().name() == "EE"


def test_invalid_vis_params(qgis_iface_clean):
dialog = add_gee_layer_dialog(qgis_iface_clean)
dialog.findChild(QtWidgets.QLineEdit, "imageId").setText("USGS/SRTMGL1_003")

dialog.findChild(QtWidgets.QTextEdit, "vizParams").setText(
"not a valid JSON string"
)

_load_gee_layer(dialog)

assert len(qgis_iface_clean.mapCanvas().layers()) == 0


def test_empty_vis_params(qgis_iface_clean):
dialog = add_gee_layer_dialog(qgis_iface_clean)
dialog.findChild(QtWidgets.QLineEdit, "imageId").setText("USGS/SRTMGL1_003")

dialog.findChild(QtWidgets.QTextEdit, "vizParams").setText("")

_load_gee_layer(dialog)

assert len(qgis_iface_clean.mapCanvas().layers()) == 1
assert qgis_iface_clean.mapCanvas().layers()[0].name() == "USGS/SRTMGL1_003"
assert qgis_iface_clean.mapCanvas().layers()[0].dataProvider().name() == "EE"
Loading