diff --git a/felt/core/__init__.py b/felt/core/__init__.py index d984d84..6d100ad 100644 --- a/felt/core/__init__.py +++ b/felt/core/__init__.py @@ -19,7 +19,7 @@ from .map_uploader import MapUploaderTask # noqa from .layer_exporter import ( # noqa LayerExporter, - ExportResult + ZippedExportResult ) from .multi_step_feedback import MultiStepFeedback # noqa from .meta import PLUGIN_METADATA_PARSER # noqa diff --git a/felt/core/layer_exporter.py b/felt/core/layer_exporter.py index a36807f..4a56a6b 100644 --- a/felt/core/layer_exporter.py +++ b/felt/core/layer_exporter.py @@ -16,9 +16,14 @@ import tempfile import uuid import zipfile +import math from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import ( + Optional, + List, + Tuple +) from qgis.PyQt.QtCore import ( QVariant, @@ -49,7 +54,10 @@ QgsSimpleLineSymbolLayer, QgsSimpleMarkerSymbolLayer, QgsEllipseSymbolLayer, - QgsReadWriteContext + QgsReadWriteContext, + QgsRasterPipe, + QgsRasterNuller, + QgsRasterRange ) from .enums import LayerExportResult @@ -59,10 +67,23 @@ @dataclass -class ExportResult: +class LayerExportDetails: """ Export results """ + representative_filename: str + filenames: List[str] + result: LayerExportResult + error_message: str + qgis_style_xml: str + style: Optional[LayerStyle] = None + + +@dataclass +class ZippedExportResult: + """ + A zipped export results + """ filename: str result: LayerExportResult error_message: str @@ -174,7 +195,7 @@ def export_layer_for_felt( self, layer: QgsMapLayer, feedback: Optional[QgsFeedback] = None - ) -> ExportResult: + ) -> ZippedExportResult: """ Exports a layer into a format acceptable for Felt :raises LayerPackagingException @@ -189,15 +210,21 @@ def export_layer_for_felt( # package into zip zip_file_path = ( (Path(str(self.temp_dir.name)) / - (Path(res.filename).stem + '.zip')).as_posix()) + (Path(res.representative_filename).stem + '.zip')).as_posix()) with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) as zipf: - zipf.write(res.filename, Path(res.filename).name) + for filename in res.filenames: + zipf.write(filename, Path(filename).name) # add QGIS layer style xml also zipf.writestr("qgis_style.xml", res.qgis_style_xml) - res.filename = zip_file_path - return res + return ZippedExportResult( + filename=zip_file_path, + result=res.result, + error_message=res.error_message, + qgis_style_xml=res.qgis_style_xml, + style=res.style + ) @staticmethod def _get_original_style_xml(layer: QgsMapLayer) -> str: @@ -212,7 +239,7 @@ def _get_original_style_xml(layer: QgsMapLayer) -> str: def export_vector_layer( self, layer: QgsVectorLayer, - feedback: Optional[QgsFeedback] = None) -> ExportResult: + feedback: Optional[QgsFeedback] = None) -> LayerExportDetails: """ Exports a vector layer into a format acceptable for Felt """ @@ -305,25 +332,25 @@ def export_vector_layer( res_layer.featureCount(), layer.featureCount()) ) - return ExportResult( - filename=dest_file, + return LayerExportDetails( + representative_filename=dest_file, + filenames=[dest_file], result=layer_export_result, error_message=error_message, qgis_style_xml=self._get_original_style_xml(layer), style=self.representative_layer_style(layer) ) - def export_raster_layer( - self, - layer: QgsRasterLayer, - feedback: Optional[QgsFeedback] = None) -> ExportResult: + def run_raster_writer(self, + layer: QgsRasterLayer, + file_name: str, + use_style: bool, + feedback: Optional[QgsFeedback] = None) \ + -> Tuple[LayerExportResult, Optional[str]]: """ - Exports a raster layer into a format acceptable for Felt + Runs a raster write operation for the layer """ - - dest_file = self.generate_file_name('.tif') - - writer = QgsRasterFileWriter(dest_file) + writer = QgsRasterFileWriter(file_name) writer.setOutputFormat('GTiff') writer.setOutputProviderKey('gdal') writer.setTiledMode(False) @@ -336,26 +363,29 @@ def export_raster_layer( ]) extent = layer.extent() - raster_pipe = layer.pipe() - projector = raster_pipe.projector() + if use_style: + raster_pipe = layer.pipe() + else: + raster_pipe = QgsRasterPipe() + raster_pipe.set(layer.dataProvider().clone()) + nuller = QgsRasterNuller() + for band in range(1, layer.dataProvider().bandCount() + 1): + additional_no_data_values = ( + layer.dataProvider().userNoDataValues( + band)) + source_no_data = layer.dataProvider().sourceNoDataValue(band) + if not math.isnan(source_no_data): + additional_no_data_values.append( + QgsRasterRange( + layer.dataProvider().sourceNoDataValue(band), + layer.dataProvider().sourceNoDataValue(band) + ) + ) + if additional_no_data_values: + nuller.setNoData(band, additional_no_data_values) + raster_pipe.insert(1, nuller) dest_crs = layer.crs() - # disable local reprojection for now - see #14 - if False: # pylint: disable=using-constant-test - dest_crs = QgsCoordinateReferenceSystem('EPSG:3857') - projector.setCrs( - layer.crs(), - dest_crs, - self.transform_context - ) - - to_3857_transform = QgsCoordinateTransform( - layer.crs(), - dest_crs, - self.transform_context - ) - extent = to_3857_transform.transformBoundingBox(extent) - width = layer.width() if feedback: block_feedback = QgsRasterBlockFeedback() @@ -388,6 +418,32 @@ def export_raster_layer( QgsRasterFileWriter.WriterError.WriteCanceled: None, }[res] + + layer_export_result = { + QgsRasterFileWriter.WriterError.NoError: + LayerExportResult.Success, + QgsRasterFileWriter.WriterError.WriteCanceled: + LayerExportResult.Canceled, + }[res] + + return layer_export_result, error_message + + def export_raster_layer( + self, + layer: QgsRasterLayer, + feedback: Optional[QgsFeedback] = None) -> LayerExportDetails: + """ + Exports a raster layer into a format acceptable for Felt + """ + raw_dest_file = self.generate_file_name('.tif') + styled_dest_file = raw_dest_file.replace('.tif', '_styled.tif') + + layer_export_result, error_message = self.run_raster_writer( + layer, + file_name=styled_dest_file, + use_style=True, + feedback=feedback) + if error_message: Logger.instance().log_error_json( { @@ -397,15 +453,31 @@ def export_raster_layer( ) raise LayerPackagingException(error_message) - layer_export_result = { - QgsRasterFileWriter.WriterError.NoError: - LayerExportResult.Success, - QgsRasterFileWriter.WriterError.WriteCanceled: - LayerExportResult.Canceled, - }[res] + filenames = [styled_dest_file] + if layer_export_result != LayerExportResult.Canceled: + # also write raw raster + + layer_export_result, error_message = self.run_raster_writer( + layer, + file_name=raw_dest_file, + use_style=False, + feedback=feedback) + + if error_message: + Logger.instance().log_error_json( + { + 'type': Logger.PACKAGING_RASTER, + 'error': 'Error packaging layer: {}'.format( + error_message) + } + ) + raise LayerPackagingException(error_message) + + filenames.append(raw_dest_file) - return ExportResult( - filename=dest_file, + return LayerExportDetails( + representative_filename=raw_dest_file, + filenames=filenames, result=layer_export_result, error_message=error_message, qgis_style_xml=self._get_original_style_xml(layer) diff --git a/felt/test/test_layer_exporter.py b/felt/test/test_layer_exporter.py index 15be143..eb40562 100644 --- a/felt/test/test_layer_exporter.py +++ b/felt/test/test_layer_exporter.py @@ -172,14 +172,17 @@ def test_raster_conversion(self): self.assertEqual(result.filename[-4:], '.zip') with zipfile.ZipFile(result.filename) as z: tif_files = [f for f in z.namelist() if f.endswith('tif')] - self.assertEqual(len(tif_files), 1) + self.assertEqual(len(tif_files), 2) + + styled_tif = [f for f in tif_files if '_styled' in f][0] + self.assertEqual( result.qgis_style_xml[:58], "" ) out_layer = QgsRasterLayer( - '/vsizip/{}/{}'.format(result.filename, tif_files[0]), + '/vsizip/{}/{}'.format(result.filename, styled_tif), 'test') self.assertTrue(out_layer.isValid()) self.assertEqual(out_layer.width(), 373) @@ -204,6 +207,27 @@ def test_raster_conversion(self): self.assertAlmostEqual(out_layer.extent().yMaximum(), 45.8117014376, 3) + raw_tif = [f for f in tif_files if '_styled' not in f][0] + out_layer = QgsRasterLayer( + '/vsizip/{}/{}'.format(result.filename, raw_tif), + 'test') + self.assertTrue(out_layer.isValid()) + self.assertEqual(out_layer.width(), 373) + self.assertEqual(out_layer.height(), 350) + self.assertEqual(out_layer.bandCount(), 1) + self.assertEqual(out_layer.dataProvider().dataType(1), + Qgis.DataType.Float32) + self.assertEqual(out_layer.crs(), + QgsCoordinateReferenceSystem('EPSG:4326')) + self.assertAlmostEqual(out_layer.extent().xMinimum(), + 18.6662979442, 3) + self.assertAlmostEqual(out_layer.extent().xMaximum(), + 18.7035979442, 3) + self.assertAlmostEqual(out_layer.extent().yMinimum(), + 45.7767014376, 3) + self.assertAlmostEqual(out_layer.extent().yMaximum(), + 45.8117014376, 3) + if __name__ == "__main__": suite = unittest.makeSuite(LayerExporterTest)