From ad1e99ad9c561a786d002b77aff3e6a0d1aab1c3 Mon Sep 17 00:00:00 2001 From: Clemens Rudert Date: Fri, 6 Sep 2024 09:44:34 +0200 Subject: [PATCH] fix precommit --- src/qgis_server_light/exporter/extract.py | 292 +++++++++++++--------- src/qgis_server_light/exporter/funcs.py | 21 +- src/qgis_server_light/interface/qgis.py | 34 ++- 3 files changed, 228 insertions(+), 119 deletions(-) diff --git a/src/qgis_server_light/exporter/extract.py b/src/qgis_server_light/exporter/extract.py index 3c8fdca..c3dfaa0 100644 --- a/src/qgis_server_light/exporter/extract.py +++ b/src/qgis_server_light/exporter/extract.py @@ -1,24 +1,45 @@ -import os import json +import os import uuid from base64 import urlsafe_b64encode -from os import environ, path -from typing import Dict, List, Tuple, Union +from os import path +from typing import List +from typing import Tuple +from typing import Union from PyQt5.QtXml import QDomDocument +from qgis.core import QgsLayerTree +from qgis.core import QgsLayerTreeGroup +from qgis.core import QgsLayerTreeLayer +from qgis.core import QgsProject +from qgis.core import QgsProviderRegistry +from qgis.core import QgsVectorLayer -from qgis_server_light.interface.qgis import ( - Raster, Vector, TreeGroup, TreeLayer, Config, BBox, Tree, MetaData, Service, Project, Field, Datasets, - Group, OgrSource, GdalSource, PostgresSource, WmsSource, WmtsSource, Crs, WfsSource, DataSource) - -from qgis_server_light.exporter.funcs import ( - extent_in_wgs84, - get_layer_type, - get_project_server_entries, -) -from qgis.core import ( - QgsProject, QgsLayerTree, QgsLayerTreeGroup, QgsLayerTreeLayer, QgsVectorLayer, QgsProviderRegistry, QgsDataSourceUri -) +from qgis_server_light.exporter.funcs import extent_in_wgs84 +from qgis_server_light.exporter.funcs import get_layer_type +from qgis_server_light.exporter.funcs import get_project_server_entries +from qgis_server_light.interface.qgis import BBox +from qgis_server_light.interface.qgis import Config +from qgis_server_light.interface.qgis import Crs +from qgis_server_light.interface.qgis import Custom +from qgis_server_light.interface.qgis import Datasets +from qgis_server_light.interface.qgis import DataSource +from qgis_server_light.interface.qgis import Field +from qgis_server_light.interface.qgis import GdalSource +from qgis_server_light.interface.qgis import Group +from qgis_server_light.interface.qgis import MetaData +from qgis_server_light.interface.qgis import OgrSource +from qgis_server_light.interface.qgis import PostgresSource +from qgis_server_light.interface.qgis import Project +from qgis_server_light.interface.qgis import Raster +from qgis_server_light.interface.qgis import Service +from qgis_server_light.interface.qgis import Tree +from qgis_server_light.interface.qgis import TreeGroup +from qgis_server_light.interface.qgis import Vector +from qgis_server_light.interface.qgis import VectorTileSource +from qgis_server_light.interface.qgis import WfsSource +from qgis_server_light.interface.qgis import WmsSource +from qgis_server_light.interface.qgis import WmtsSource def extract_fields(layer: QgsVectorLayer) -> List[Field]: @@ -30,37 +51,36 @@ def extract_fields(layer: QgsVectorLayer) -> List[Field]: def create_unified_short_name(name: str, path: list[str]): short_name_part = path + [name] - return '.'.join(short_name_part) + return ".".join(short_name_part) def extract_save_layer( - project: QgsProject, - child: QgsLayerTreeLayer, - tree: Tree, - datasets: Datasets, - path: list[str], - unify_layer_names_by_group=False + project: QgsProject, + child: QgsLayerTreeLayer, + tree: Tree, + datasets: Datasets, + path: list[str], + unify_layer_names_by_group=False, ): """Save the given layer to the output path.""" if isinstance(child, QgsLayerTreeLayer): child = child.layer() - qml_name = os.path.join('/tmp', f'{str(uuid.uuid4())}.qml') + os.path.join("/tmp", f"{str(uuid.uuid4())}.qml") style_doc = QDomDocument() - style = child.exportNamedStyle(style_doc) + child.exportNamedStyle(style_doc) layer_type = get_layer_type(child) decoded = QgsProviderRegistry.instance().decodeUri( - child.providerType(), - child.dataProvider().dataSourceUri() + child.providerType(), child.dataProvider().dataSourceUri() ) for key in decoded: - if str(decoded[key]) == 'None': + if str(decoded[key]) == "None": decoded[key] = None - elif str(decoded[key]) == 'NULL': + elif str(decoded[key]) == "NULL": decoded[key] = None else: decoded[key] = str(decoded[key]) - if key == 'path': - decoded[key] = decoded[key].replace(f'{project.readPath("./")}/', '') + if key == "path": + decoded[key] = decoded[key].replace(f'{project.readPath("./")}/', "") if unify_layer_names_by_group: short_name = create_unified_short_name(child.shortName(), path) else: @@ -68,61 +88,64 @@ def extract_save_layer( crs = Crs( postgis_srid=child.dataProvider().crs().postgisSrid(), auth_id=child.dataProvider().crs().authid(), - ogc_uri=child.dataProvider().crs().toOgcUri() + ogc_uri=child.dataProvider().crs().toOgcUri(), ) - extent_wgs_84 = child.wgs84Extent(forceRecalculate=True) + extent_wgs_84 = extent_in_wgs84(project, child) bbox_wgs84 = BBox( - x_min=extent_wgs_84.xMinimum(), - x_max=extent_wgs_84.xMaximum(), - y_min=extent_wgs_84.yMaximum(), - y_max=extent_wgs_84.yMaximum() + x_min=extent_wgs_84[0], + x_max=extent_wgs_84[2], + y_min=extent_wgs_84[1], + y_max=extent_wgs_84[3], ) - if layer_type == 'vector': - child.updateExtents() extent = child.extent() - bbox = BBox.from_list([ - extent.xMinimum(), - extent.yMinimum(), - 0.0, - extent.xMaximum(), - extent.yMaximum(), - 0.0 - ]) - if layer_type == 'vector': - if child.providerType().lower() == 'ogr': + bbox = BBox.from_list( + [ + extent.xMinimum(), + extent.yMinimum(), + 0.0, + extent.xMaximum(), + extent.yMaximum(), + 0.0, + ] + ) + if layer_type == "vector": + if child.providerType().lower() == "ogr": source = DataSource( ogr=OgrSource( - path=decoded['path'], - layer_name=decoded['layerName'], - layer_id=decoded['layerId'] + path=decoded["path"], + layer_name=decoded["layerName"], + layer_id=decoded["layerId"], ) ) - elif child.providerType().lower() == 'postgres': + elif child.providerType().lower() == "postgres": source = DataSource( postgres=PostgresSource( - dbname=decoded['dbname'], - geometry_column=decoded['geometrycolumn'], - host=decoded['host'], - key=decoded['key'], - password=decoded['password'], - port=decoded['port'], - schema=decoded['schema'], - srid=decoded['srid'], - table=decoded['table'], - type=decoded['type'], - username=decoded['username'] + dbname=decoded["dbname"], + geometry_column=decoded["geometrycolumn"], + host=decoded["host"], + key=decoded["key"], + password=decoded["password"], + port=decoded["port"], + schema=decoded["schema"], + srid=decoded["srid"], + table=decoded["table"], + type=decoded["type"], + username=decoded["username"], ) ) - elif child.providerType().lower() == 'wfs': + + elif child.providerType().lower() == "wfs": # TODO: Correctly implement source! source = WfsSource() else: - raise NotImplementedError + raise NotImplementedError( + f"Unknown provider type: {child.providerType().lower()}" + ) fields = extract_fields(child) datasets.vector.append( Vector( - path=child.source().replace(f'{project.readPath("./")}/', ''), + path=child.source().replace(f'{project.readPath("./")}/', ""), name=short_name, title=child.title(), style=urlsafe_b64encode(style_doc.toByteArray()).decode(), @@ -132,51 +155,80 @@ def extract_save_layer( source=source, id=child.id(), crs=crs, - bbox=bbox + bbox=bbox, ) ) - elif layer_type == 'raster': - if child.providerType() == 'gdal': + elif layer_type == "raster": + if child.providerType() == "gdal": source = DataSource( - gdal=GdalSource( - path=decoded['path'], - layer_name=decoded['layerName'] - ) + gdal=GdalSource(path=decoded["path"], layer_name=decoded["layerName"]) ) - elif child.providerType() == 'wms': - if 'tileMatrixSet' in decoded: + elif child.providerType() == "wms": + if "tileMatrixSet" in decoded: source = DataSource( wmts=WmtsSource( - contextual_wms_legend=decoded['contextualWMSLegend'], - crs=decoded['crs'], - dpi_mode=decoded['dpiMode'], - feature_count=decoded['featureCount'], - format=decoded['format'], - layers=decoded['layers'], - styles=decoded['styles'], - tile_dimensions=decoded['tileDimensions'], - tile_matrix_set=decoded['tileMatrixSet'], - tile_pixel_ratio=decoded['tilePixelRatio'], - url=decoded['url'] + contextual_wms_legend=decoded["contextualWMSLegend"], + crs=decoded["crs"], + dpi_mode=decoded["dpiMode"], + feature_count=decoded["featureCount"], + format=decoded["format"], + layers=decoded["layers"], + styles=decoded["styles"], + tile_dimensions=decoded["tileDimensions"], + tile_matrix_set=decoded["tileMatrixSet"], + tile_pixel_ratio=decoded["tilePixelRatio"], + url=decoded["url"], ) ) else: source = DataSource( wms=WmsSource( - contextual_wms_legend=decoded['contextualWMSLegend'], - crs=decoded['crs'], - dpi_mode=decoded['dpiMode'], - feature_count=decoded['featureCount'], - format=decoded['format'], - layers=decoded['layers'], - url=decoded['url'] + contextual_wms_legend=decoded["contextualWMSLegend"], + crs=decoded["crs"], + dpi_mode=decoded["dpiMode"], + feature_count=decoded["featureCount"], + format=decoded["format"], + layers=decoded["layers"], + url=decoded["url"], ) ) else: - raise NotImplementedError + raise NotImplementedError( + f"Unknown provider type: {child.providerType().lower()}" + ) datasets.raster.append( Raster( - path=child.source().replace(f'{project.readPath("./")}/', ''), + path=child.source().replace(f'{project.readPath("./")}/', ""), + name=short_name, + title=child.title(), + style=urlsafe_b64encode(style_doc.toByteArray()).decode(), + driver=child.providerType(), + bbox_wgs84=bbox_wgs84, + source=source, + id=child.id(), + crs=crs, + bbox=bbox, + ) + ) + elif layer_type == "custom": + if child.providerType().lower() == "xyzvectortiles": + source = DataSource( + vector_tile=VectorTileSource( + styleUrl=decoded["styleUrl"], + url=decoded["url"], + zmax=decoded["zmax"], + zmin=decoded["zmin"], + type=decoded["type"], + ) + ) + else: + raise NotImplementedError( + f"Unknown provider type: {child.providerType().lower()}" + ) + # TODO: make this more configurable + datasets.custom.append( + Custom( + path=child.source().replace(f'{project.readPath("./")}/', ""), name=short_name, title=child.title(), style=urlsafe_b64encode(style_doc.toByteArray()).decode(), @@ -185,14 +237,20 @@ def extract_save_layer( source=source, id=child.id(), crs=crs, - bbox=bbox + bbox=bbox, ) ) else: raise NotImplementedError(f'Unknown layer_type "{layer_type}"') -def extract_group(group: QgsLayerTreeGroup, tree: Tree, datasets: Datasets, path: list[str], unify_layer_names_by_group=False): +def extract_group( + group: QgsLayerTreeGroup, + tree: Tree, + datasets: Datasets, + path: list[str], + unify_layer_names_by_group=False, +): """Collects data pertaining to a QGIS layer tree group.""" children = [] for child in group.children(): @@ -200,33 +258,34 @@ def extract_group(group: QgsLayerTreeGroup, tree: Tree, datasets: Datasets, path children.append(child.customProperty("wmsShortName")) else: if unify_layer_names_by_group: - children.append(create_unified_short_name(child.layer().shortName(), path)) + children.append( + create_unified_short_name(child.layer().shortName(), path) + ) else: children.append(child.layer().shortName()) tree.members.append( - TreeGroup( - name=group.customProperty("wmsShortName"), - children=children - ) + TreeGroup(name=group.customProperty("wmsShortName"), children=children) ) datasets.group.append( Group( name=group.customProperty("wmsShortName"), - title=group.customProperty("wmsTitle") + title=group.customProperty("wmsTitle"), ) ) def extract_entities( - project: QgsProject, - entity: Union[QgsLayerTree, QgsLayerTreeGroup, QgsLayerTreeLayer], - tree: Tree, - datasets: Datasets, - path: list[str], - unify_layer_names_by_group=False + project: QgsProject, + entity: Union[QgsLayerTree, QgsLayerTreeGroup, QgsLayerTreeLayer], + tree: Tree, + datasets: Datasets, + path: list[str], + unify_layer_names_by_group=False, ): if isinstance(entity, QgsLayerTreeLayer): - extract_save_layer(project, entity, tree, datasets, path, unify_layer_names_by_group) + extract_save_layer( + project, entity, tree, datasets, path, unify_layer_names_by_group + ) # If the entity has an attribute `children`, assume it's a group elif isinstance(entity, QgsLayerTreeGroup) or isinstance(entity, QgsLayerTree): @@ -234,7 +293,9 @@ def extract_entities( path = path + [entity.customProperty("wmsShortName")] extract_group(entity, tree, datasets, path, unify_layer_names_by_group) for child in entity.children(): - extract_entities(project, child, tree, datasets, path, unify_layer_names_by_group) + extract_entities( + project, child, tree, datasets, path, unify_layer_names_by_group + ) def extract_metadata(project) -> MetaData: @@ -248,7 +309,7 @@ def extract_metadata(project) -> MetaData: categories=_meta.categories(), creationDateTime=_meta.creationDateTime().toPyDateTime().isoformat(), language=_meta.language(), - links=_meta.links() + links=_meta.links(), ) @@ -269,9 +330,9 @@ def get_project_root(path_to_project) -> Tuple[QgsProject, QgsLayerTree]: def prepare_project_name(project: QgsProject) -> tuple[str, str]: # TODO: Find a good approach to recognize different "versions" of a project. name = project.baseName() - parts = name.split('.') + parts = name.split(".") version = parts.pop(0) - assembled_name = '.'.join(parts) + assembled_name = ".".join(parts) if assembled_name == "": assembled_name = project.title() return version, assembled_name @@ -290,8 +351,7 @@ def extract(path_to_project: str, unify_layer_names_by_group=False) -> Config: project=Project(name=assembled_name, version=version), meta_data=extract_metadata(project), tree=tree, - datasets=datasets + datasets=datasets, ) extract_entities(project, root, tree, datasets, [], unify_layer_names_by_group) return config - diff --git a/src/qgis_server_light/exporter/funcs.py b/src/qgis_server_light/exporter/funcs.py index f5584c4..1b7dd51 100644 --- a/src/qgis_server_light/exporter/funcs.py +++ b/src/qgis_server_light/exporter/funcs.py @@ -1,8 +1,18 @@ from functools import reduce from itertools import zip_longest -from typing import Any, Dict, List, Union +from typing import Any +from typing import Dict +from typing import List +from typing import Union -from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsVectorLayer, QgsRasterLayer +from qgis._core import QgsMeshLayer +from qgis._core import QgsPointCloudLayer +from qgis._core import QgsTiledSceneLayer +from qgis._core import QgsVectorTileLayer +from qgis.core import QgsCoordinateReferenceSystem +from qgis.core import QgsCoordinateTransform +from qgis.core import QgsRasterLayer +from qgis.core import QgsVectorLayer def compose(*fs): @@ -54,6 +64,13 @@ def get_layer_type(layer: QgsVectorLayer | QgsRasterLayer) -> str: return "raster" elif isinstance(layer, QgsVectorLayer): return "vector" + elif ( + isinstance(layer, QgsVectorTileLayer) + or isinstance(layer, QgsTiledSceneLayer) + or isinstance(layer, QgsPointCloudLayer) + or isinstance(layer, QgsMeshLayer) + ): + return "custom" else: raise TypeError(f"Not implemented: {layer.type()}") diff --git a/src/qgis_server_light/interface/qgis.py b/src/qgis_server_light/interface/qgis.py index b81e329..2a3ab47 100644 --- a/src/qgis_server_light/interface/qgis.py +++ b/src/qgis_server_light/interface/qgis.py @@ -179,6 +179,17 @@ class PostgresSource(Source): ) +@dataclass +class VectorTileSource(Source): + styleUrl: str = field( + metadata={"name": "styleUrl", "type": "Element", "required": True} + ) + type: str = field(metadata={"name": "Type", "type": "Element", "required": True}) + url: str = field(metadata={"name": "Url", "type": "Element", "required": True}) + zmax: str = field(metadata={"name": "Zmax", "type": "Element", "required": True}) + zmin: str = field(metadata={"name": "Zmin", "type": "Element", "required": True}) + + @dataclass class Crs: auth_id: str = field( @@ -214,6 +225,10 @@ class DataSource: wfs: WfsSource = field( default=None, metadata={"name": "Wfs", "type": "Element", "required": False} ) + vector_tile: VectorTileSource = field( + default=None, + metadata={"name": "VectorTile", "type": "Element", "required": False}, + ) @dataclass @@ -238,17 +253,30 @@ class DataSet(AbstractDataset): @dataclass class Raster(DataSet): - pass + """ + A real QGIS Raster dataset. That are usually all `QgsRasterLayer` (in opposition to `QgsVectorTileLayer` + which is not a real `QgsRasterLayer`. + """ @dataclass class Vector(DataSet): + """ + A real QGIS Vector dataset. That are usually all `QgsVectorLayer` (in opposition to `QgsVectorTileLayer` + which is not a real `QgsVectorLayer`. + """ + fields: Optional[List[Field]] = field( default_factory=list, metadata={"name": "Fields", "type": "Element", "required": True}, ) +@dataclass +class Custom(DataSet): + pass + + @dataclass class Group(AbstractDataset): pass @@ -365,6 +393,10 @@ class Datasets: default_factory=list, metadata={"name": "RasterDataset", "type": "Element", "required": False}, ) + custom: list[Custom] = field( + default_factory=list, + metadata={"name": "Custom", "type": "Element", "required": False}, + ) group: list[Group] = field( default_factory=list, metadata={"name": "GroupDataset", "type": "Element", "required": False},