From e1e3573d3ff74dc9808594a6929f68e48bb3d5da Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Tue, 25 Jun 2024 17:17:02 +0200 Subject: [PATCH 1/8] Add support for datetime parameter --- qsa-api/qsa_api/api/projects.py | 12 +++++++++++- qsa-api/qsa_api/project.py | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/qsa-api/qsa_api/api/projects.py b/qsa-api/qsa_api/api/projects.py index 823ee6a..2718cd8 100644 --- a/qsa-api/qsa_api/api/projects.py +++ b/qsa-api/qsa_api/api/projects.py @@ -6,6 +6,8 @@ from jsonschema.exceptions import ValidationError from flask import send_file, Blueprint, jsonify, request +from qgis.PyQt.QtCore import QDateTime + from ..wms import WMS from ..project import QSAProject @@ -271,6 +273,7 @@ def project_add_layer(name): "crs": {"type": "number"}, "type": {"type": "string"}, "overview": {"type": "boolean"}, + "datetime": {"type": "string"}, }, } @@ -292,8 +295,15 @@ def project_add_layer(name): if "overview" in data: overview = data["overview"] + datetime = None + if "datetime" in data: + # check format "yyyy-MM-dd HH:mm:ss" + datetime = QDateTime.fromString(data["datetime"], "yyyy-MM-dd HH:mm:ss") + if not datetime.isValid(): + return {"error": "Invalid datetime"}, 415 + rc, err = project.add_layer( - data["datasource"], data["type"], data["name"], crs, overview + data["datasource"], data["type"], data["name"], crs, overview, datetime ) if rc: return jsonify(rc), 201 diff --git a/qsa-api/qsa_api/project.py b/qsa-api/qsa_api/project.py index 36c22ec..0e50465 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -5,7 +5,7 @@ import sqlite3 from pathlib import Path -from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtCore import Qt, QDateTime from qgis.core import ( Qgis, QgsSymbol, @@ -18,12 +18,14 @@ QgsVectorLayer, QgsRasterLayer, QgsMarkerSymbol, + QgsDateTimeRange, QgsRasterMinMaxOrigin, QgsContrastEnhancement, QgsSingleSymbolRenderer, QgsSimpleFillSymbolLayer, QgsSimpleLineSymbolLayer, QgsSimpleMarkerSymbolLayer, + QgsRasterLayerTemporalProperties, ) from .mapproxy import QSAMapProxy @@ -321,6 +323,7 @@ def add_layer( name: str, epsg_code: int, overview: bool, + datetime: QDateTime ) -> (bool, str): t = self._layer_type(layer_type) if t is None: @@ -349,6 +352,15 @@ def add_layer( return False, err else: self.debug("Overviews already exist") + + if datetime: + self.debug("Activate temporal properties") + mode = QgsRasterLayerTemporalProperties.ModeFixedTemporalRange + props = lyr.temporalProperties() + props.setMode(mode) + dt_range = QgsDateTimeRange(datetime, datetime) + props.setFixedTemporalRange(dt_range) + props.setIsActive(True) else: return False, "Invalid layer type" From d7cca1b4429f592722b96957a42f76b7ddbba33b Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 13:43:13 +0200 Subject: [PATCH 2/8] Add MapProxy support for datetime --- qsa-api/qsa_api/mapproxy/mapproxy.py | 6 +++++- qsa-api/qsa_api/project.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 26da2f0..5bf4915 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -50,7 +50,7 @@ def clear_cache(self, layer_name: str) -> None: shutil.rmtree(d) def add_layer( - self, name: str, bbox: list, srs: int, is_raster: bool + self, name: str, bbox: list, srs: int, is_raster: bool, datetime: QDateTime | None ) -> (bool, str): if self.cfg is None: return False, "Invalid MapProxy configuration" @@ -61,6 +61,10 @@ def add_layer( self.cfg["sources"] = {} lyr = {"name": name, "title": name, "sources": [f"{name}_cache"]} + if datetime and is_raster: + lyr["dimensions"] = {} + lyr["dimensions"]["time"] = {"values": [datetime.toString()]} + self.cfg["layers"].append(lyr) c = {"grids": ["webmercator"], "sources": [f"{name}_wms"]} diff --git a/qsa-api/qsa_api/project.py b/qsa-api/qsa_api/project.py index 0e50465..abe893c 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -323,7 +323,7 @@ def add_layer( name: str, epsg_code: int, overview: bool, - datetime: QDateTime + datetime: QDateTime | None ) -> (bool, str): t = self._layer_type(layer_type) if t is None: @@ -415,7 +415,7 @@ def add_layer( return False, err rc, err = mp.add_layer( - name, bbox, epsg_code, t == Qgis.LayerType.Raster + name, bbox, epsg_code, t == Qgis.LayerType.Raster, datetime ) if not rc: return False, err From 132a8ca9696b254b0893bb90fc71ec170d46039c Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 13:45:02 +0200 Subject: [PATCH 3/8] Forward TIME parameter to map server --- qsa-api/qsa_api/mapproxy/mapproxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 5bf4915..905bdb5 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -83,6 +83,9 @@ def add_layer( }, "coverage": {"bbox": bbox, "srs": f"EPSG:{srs}"}, } + if datetime and is_raster: + s["forward_req_params"] = ['TIME'] + self.cfg["sources"][f"{name}_wms"] = s return True, "" From 3e64a26f953f42211be55e4b78b6467d5b4c3617 Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 13:55:55 +0200 Subject: [PATCH 4/8] Fix import --- qsa-api/qsa_api/mapproxy/mapproxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 905bdb5..3c89904 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -4,6 +4,8 @@ import shutil from pathlib import Path +from qgis.PyQt.QtCore import QDateTime + from ..utils import config, qgisserver_base_url From 272e245ac2b2f486a2afa055b8a3d2a89fd94d27 Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 14:01:11 +0200 Subject: [PATCH 5/8] Format datetime in MapProxy config file --- qsa-api/qsa_api/mapproxy/mapproxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 3c89904..79923ef 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -64,8 +64,9 @@ def add_layer( lyr = {"name": name, "title": name, "sources": [f"{name}_cache"]} if datetime and is_raster: + fmt = "yyyy-MM-dd HH:mm:ss" lyr["dimensions"] = {} - lyr["dimensions"]["time"] = {"values": [datetime.toString()]} + lyr["dimensions"]["time"] = {"values": [datetime.toString(fmt)]} self.cfg["layers"].append(lyr) From 3e09c8fee76d1f0aa51dda16f2e8d5f04e5b1646 Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 14:09:45 +0200 Subject: [PATCH 6/8] Iso format --- qsa-api/qsa_api/mapproxy/mapproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 79923ef..fb3a679 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -64,7 +64,7 @@ def add_layer( lyr = {"name": name, "title": name, "sources": [f"{name}_cache"]} if datetime and is_raster: - fmt = "yyyy-MM-dd HH:mm:ss" + fmt = "yyyy-MM-ddTHH:mm:ss" lyr["dimensions"] = {} lyr["dimensions"]["time"] = {"values": [datetime.toString(fmt)]} From 4e8bcf4ba1000bf41e3bc5be9ad7f515ae7e3aca Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 14:13:38 +0200 Subject: [PATCH 7/8] Qt ISO Date --- qsa-api/qsa_api/mapproxy/mapproxy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index fb3a679..5ac7dec 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -4,7 +4,7 @@ import shutil from pathlib import Path -from qgis.PyQt.QtCore import QDateTime +from qgis.PyQt.QtCore import Qt, QDateTime from ..utils import config, qgisserver_base_url @@ -64,9 +64,8 @@ def add_layer( lyr = {"name": name, "title": name, "sources": [f"{name}_cache"]} if datetime and is_raster: - fmt = "yyyy-MM-ddTHH:mm:ss" lyr["dimensions"] = {} - lyr["dimensions"]["time"] = {"values": [datetime.toString(fmt)]} + lyr["dimensions"]["time"] = {"values": [datetime.toString(Qt.ISODate)]} self.cfg["layers"].append(lyr) From d37625f09fe0e50f515dc0e7e29ced28f8853cba Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Wed, 26 Jun 2024 14:20:54 +0200 Subject: [PATCH 8/8] Format --- qsa-api/qsa_api/api/projects.py | 11 ++++-- qsa-api/qsa_api/api/symbology.py | 8 ++--- qsa-api/qsa_api/app.py | 1 + qsa-api/qsa_api/mapproxy/mapproxy.py | 13 ++++++-- qsa-api/qsa_api/project.py | 2 +- qsa-api/qsa_api/raster/renderer.py | 50 ++++++++++++++++++++++------ 6 files changed, 64 insertions(+), 21 deletions(-) diff --git a/qsa-api/qsa_api/api/projects.py b/qsa-api/qsa_api/api/projects.py index 2718cd8..2e302fa 100644 --- a/qsa-api/qsa_api/api/projects.py +++ b/qsa-api/qsa_api/api/projects.py @@ -298,12 +298,19 @@ def project_add_layer(name): datetime = None if "datetime" in data: # check format "yyyy-MM-dd HH:mm:ss" - datetime = QDateTime.fromString(data["datetime"], "yyyy-MM-dd HH:mm:ss") + datetime = QDateTime.fromString( + data["datetime"], "yyyy-MM-dd HH:mm:ss" + ) if not datetime.isValid(): return {"error": "Invalid datetime"}, 415 rc, err = project.add_layer( - data["datasource"], data["type"], data["name"], crs, overview, datetime + data["datasource"], + data["type"], + data["name"], + crs, + overview, + datetime, ) if rc: return jsonify(rc), 201 diff --git a/qsa-api/qsa_api/api/symbology.py b/qsa-api/qsa_api/api/symbology.py index 419d29c..76d2786 100644 --- a/qsa-api/qsa_api/api/symbology.py +++ b/qsa-api/qsa_api/api/symbology.py @@ -85,10 +85,10 @@ def symbology_raster_singlebandpseudocolor(): props = {} props["band"] = {"band": 1, "min": 0.0, "max": 1.0} props["ramp"] = { - "name" : f"Spectral ({ramps})", - "color1": "0,0,0,255", - "color2": "255,255,255,255", - "interpolation": "Linear (Linear, Discrete, Exact)" + "name": f"Spectral ({ramps})", + "color1": "0,0,0,255", + "color2": "255,255,255,255", + "interpolation": "Linear (Linear, Discrete, Exact)", } props["contrast_enhancement"] = { "limits_min_max": "MinMax (MinMax, UserDefined)", diff --git a/qsa-api/qsa_api/app.py b/qsa-api/qsa_api/app.py index 9d85b24..4df2563 100644 --- a/qsa-api/qsa_api/app.py +++ b/qsa-api/qsa_api/app.py @@ -34,6 +34,7 @@ def __init__(self) -> None: def run(self): app.run(host="0.0.0.0", threaded=False) + qsa = QSA() diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 5ac7dec..c916533 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -52,7 +52,12 @@ def clear_cache(self, layer_name: str) -> None: shutil.rmtree(d) def add_layer( - self, name: str, bbox: list, srs: int, is_raster: bool, datetime: QDateTime | None + self, + name: str, + bbox: list, + srs: int, + is_raster: bool, + datetime: QDateTime | None, ) -> (bool, str): if self.cfg is None: return False, "Invalid MapProxy configuration" @@ -65,7 +70,9 @@ def add_layer( lyr = {"name": name, "title": name, "sources": [f"{name}_cache"]} if datetime and is_raster: lyr["dimensions"] = {} - lyr["dimensions"]["time"] = {"values": [datetime.toString(Qt.ISODate)]} + lyr["dimensions"]["time"] = { + "values": [datetime.toString(Qt.ISODate)] + } self.cfg["layers"].append(lyr) @@ -86,7 +93,7 @@ def add_layer( "coverage": {"bbox": bbox, "srs": f"EPSG:{srs}"}, } if datetime and is_raster: - s["forward_req_params"] = ['TIME'] + s["forward_req_params"] = ["TIME"] self.cfg["sources"][f"{name}_wms"] = s diff --git a/qsa-api/qsa_api/project.py b/qsa-api/qsa_api/project.py index abe893c..40ce332 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -323,7 +323,7 @@ def add_layer( name: str, epsg_code: int, overview: bool, - datetime: QDateTime | None + datetime: QDateTime | None, ) -> (bool, str): t = self._layer_type(layer_type) if t is None: diff --git a/qsa-api/qsa_api/raster/renderer.py b/qsa-api/qsa_api/raster/renderer.py index 6f4cbc2..2e66bef 100644 --- a/qsa-api/qsa_api/raster/renderer.py +++ b/qsa-api/qsa_api/raster/renderer.py @@ -25,7 +25,9 @@ class RasterSymbologyRenderer: class Type(Enum): SINGLE_BAND_GRAY = QgsSingleBandGrayRenderer(None, 1).type() - SINGLE_BAND_PSEUDOCOLOR = QgsSingleBandPseudoColorRenderer(None, 1).type() + SINGLE_BAND_PSEUDOCOLOR = QgsSingleBandPseudoColorRenderer( + None, 1 + ).type() MULTI_BAND_COLOR = QgsMultiBandColorRenderer(None, 1, 1, 1).type() def __init__(self, name: str) -> None: @@ -46,7 +48,9 @@ def __init__(self, name: str) -> None: self.renderer = QgsSingleBandGrayRenderer(None, 1) elif name == RasterSymbologyRenderer.Type.MULTI_BAND_COLOR.value: self.renderer = QgsMultiBandColorRenderer(None, 1, 1, 1) - elif name == RasterSymbologyRenderer.Type.SINGLE_BAND_PSEUDOCOLOR.value: + elif ( + name == RasterSymbologyRenderer.Type.SINGLE_BAND_PSEUDOCOLOR.value + ): self.renderer = QgsSingleBandPseudoColorRenderer(None, 1) @property @@ -127,7 +131,10 @@ def style_to_json(path: Path) -> (dict, str): props = RasterSymbologyRenderer._multibandcolor_properties( renderer ) - elif renderer_type == RasterSymbologyRenderer.Type.SINGLE_BAND_PSEUDOCOLOR: + elif ( + renderer_type + == RasterSymbologyRenderer.Type.SINGLE_BAND_PSEUDOCOLOR + ): props = RasterSymbologyRenderer._singlebandpseudocolor_properties( renderer ) @@ -253,8 +260,12 @@ def _singlebandpseudocolor_properties(renderer) -> dict: props["ramp"] = {} shader_fct = renderer.shader().rasterShaderFunction() - color_1 = shader_fct.sourceColorRamp().properties()["color1"].split("rgb")[0] - color_2 = shader_fct.sourceColorRamp().properties()["color2"].split("rgb")[0] + color_1 = ( + shader_fct.sourceColorRamp().properties()["color1"].split("rgb")[0] + ) + color_2 = ( + shader_fct.sourceColorRamp().properties()["color2"].split("rgb")[0] + ) props["ramp"]["color1"] = color_1 props["ramp"]["color2"] = color_2 @@ -294,21 +305,30 @@ def _refresh_min_max_multibandcolor(self, layer: QgsRasterLayer) -> None: if min_max_origin == QgsRasterMinMaxOrigin.Limits.MinMax: red_band = renderer.redBand() red_stats = layer.dataProvider().bandStatistics( - red_band, QgsRasterBandStats.Min | QgsRasterBandStats.Max, layer.extent(), 250000 + red_band, + QgsRasterBandStats.Min | QgsRasterBandStats.Max, + layer.extent(), + 250000, ) red_ce.setMinimumValue(red_stats.minimumValue) red_ce.setMaximumValue(red_stats.maximumValue) green_band = renderer.greenBand() green_stats = layer.dataProvider().bandStatistics( - green_band, QgsRasterBandStats.Min | QgsRasterBandStats.Max, layer.extent(), 250000 + green_band, + QgsRasterBandStats.Min | QgsRasterBandStats.Max, + layer.extent(), + 250000, ) green_ce.setMinimumValue(green_stats.minimumValue) green_ce.setMaximumValue(green_stats.maximumValue) blue_band = renderer.blueBand() blue_stats = layer.dataProvider().bandStatistics( - blue_band, QgsRasterBandStats.Min | QgsRasterBandStats.Max, layer.extent(), 250000 + blue_band, + QgsRasterBandStats.Min | QgsRasterBandStats.Max, + layer.extent(), + 250000, ) blue_ce.setMinimumValue(blue_stats.minimumValue) blue_ce.setMaximumValue(blue_stats.maximumValue) @@ -333,7 +353,10 @@ def _refresh_min_max_singlebandgray(self, layer: QgsRasterLayer) -> None: if min_max_origin == QgsRasterMinMaxOrigin.Limits.MinMax: # Accuracy : estimate stats = layer.dataProvider().bandStatistics( - 1, QgsRasterBandStats.Min | QgsRasterBandStats.Max, layer.extent(), 250000 + 1, + QgsRasterBandStats.Min | QgsRasterBandStats.Max, + layer.extent(), + 250000, ) ce.setMinimumValue(stats.minimumValue) @@ -341,13 +364,18 @@ def _refresh_min_max_singlebandgray(self, layer: QgsRasterLayer) -> None: layer.renderer().setContrastEnhancement(ce) - def _refresh_min_max_singlebandpseudocolor(self, layer: QgsRasterLayer) -> None: + def _refresh_min_max_singlebandpseudocolor( + self, layer: QgsRasterLayer + ) -> None: # compute min/max min_max_origin = layer.renderer().minMaxOrigin().limits() if min_max_origin == QgsRasterMinMaxOrigin.Limits.MinMax: # Accuracy : estimate stats = layer.dataProvider().bandStatistics( - 1, QgsRasterBandStats.Min | QgsRasterBandStats.Max, layer.extent(), 250000 + 1, + QgsRasterBandStats.Min | QgsRasterBandStats.Max, + layer.extent(), + 250000, ) layer.renderer().setClassificationMin(stats.minimumValue)