# -*- coding: utf-8 -*- """ /*************************************************************************** QFieldSync processing provider ------------------- begin : 2016-10-05 copyright : (C) 2016 by OPENGIS.ch email : matthias@opengis.ch ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ """ from processing.core.outputs import OutputRaster from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm from qgis.PyQt.QtGui import QImage, QPainter from qgis.PyQt.QtCore import QSize from qgis.core import ( QgsMapSettings, QgsMapRendererCustomPainterJob, QgsRectangle, QgsProject, QgsProcessingException, QgsProcessingParameterExtent, QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterRasterLayer, QgsProcessingOutputRasterLayer, QgsProcessingParameterRasterDestination ) import qgis import osgeo.gdal import os import tempfile import math __author__ = 'Matthias Kuhn' __date__ = '2016-10-05' __copyright__ = '(C) 2016 by OPENGIS.ch' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' class RasterizeAlgorithm(QgisAlgorithm): """ """ # Constants used to refer to parameters and outputs. They will be # used when calling the algorithm from another algorithm, or when # calling from the QGIS console. OUTPUT_LAYER = 'OUTPUT_LAYER' MAP_THEME = 'MAP_THEME' LAYER = 'LAYER' EXTENT = 'EXTENT' TILE_SIZE = 'TILE_SIZE' MAP_UNITS_PER_PIXEL = 'MAP_UNITS_PER_PIXEL' def __init__(self): super().__init__() def initAlgorithm(self, config=None): """Here we define the inputs and output of the algorithm, along with some other properties. """ # The parameters self.addParameter( QgsProcessingParameterString(self.MAP_THEME, description=self.tr( 'Map theme to render.'), defaultValue=None, optional=True)) self.addParameter( QgsProcessingParameterRasterLayer(self.LAYER, description=self.tr( 'Layer to render. Will only be used if the map theme is not ' 'set. ' 'If both, map theme and layer are not ' 'set, the current map content will be rendered.'), optional=True)) self.addParameter( QgsProcessingParameterExtent(self.EXTENT, description=self.tr( 'The minimum extent to render. Will internally be extended to ' 'be ' 'a multiple of the tile sizes.'))) self.addParameter( QgsProcessingParameterNumber(self.TILE_SIZE, self.tr('Tile size'), defaultValue=1024)) self.addParameter(QgsProcessingParameterNumber(self.MAP_UNITS_PER_PIXEL, self.tr( 'Map units per ' 'pixel'), defaultValue=100)) # We add a raster layer as output self.addParameter(QgsProcessingParameterRasterDestination( self.OUTPUT_LAYER, self.tr( 'Output layer'))) def name(self): # Unique (non-user visible) name of algorithm return 'Rasterize' def displayName(self): # The name that the user will see in the toolbox return self.tr('Convert map to raster') def group(self): return self.tr('Raster tools') # def processAlgorithm(self, progress): def processAlgorithm(self, parameters, context, feedback): """Here is where the processing itself takes place.""" # The first thing to do is retrieve the values of the parameters # entered by the user map_theme = self.parameterAsString(parameters, self.MAP_THEME, context) layer = self.parameterAsString(parameters, self.LAYER, context) extent = self.parameterAsExtent(parameters, self.EXTENT, context) tile_size = self.parameterAsInt(parameters, self.TILE_SIZE, context) mupp = self.parameterAsInt(parameters, self.MAP_UNITS_PER_PIXEL, context) output_layer = self.parameterAsOutputLayer(parameters, self.OUTPUT_LAYER, context) # This probably affects the whole system but it's a lot nicer osgeo.gdal.UseExceptions() tile_set = TileSet(map_theme, layer, extent, tile_size, mupp, output_layer, qgis.utils.iface.mapCanvas().mapSettings()) tile_set.render() return {self.OUTPUT_LAYER: output_layer} class TileSet(): """ A set of tiles /home/mario/Dropbox/workspace/marioba/QGIS/python/plugins/processing/tests/testdata/ """ def __init__(self, map_theme, layer, extent, tile_size, mupp, output, map_settings): """ :param map_theme: :param extent: :param layer: :param tile_size: :param mupp: :param output: :param map_settings: Map canvas map settings used for some fallback values and CRS """ self.extent = extent self.mupp = mupp self.tile_size = tile_size # TODO: Check if file exists and update instead? driver = self.getDriverForFile(output) if not driver: raise QgsProcessingException( u'Could not load GDAL driver for file {}'.format(output)) crs = map_settings.destinationCrs() self.x_tile_count = math.ceil(extent.width() / mupp / tile_size) self.y_tile_count = math.ceil(extent.height() / mupp / tile_size) xsize = self.x_tile_count * tile_size ysize = self.y_tile_count * tile_size self.dataset = driver.Create(output, xsize, ysize, 3) # 3 bands self.dataset.SetProjection(str(crs.toWkt())) self.dataset.SetGeoTransform( [extent.xMinimum(), mupp, 0, extent.yMaximum(), 0, -mupp]) self.image = QImage(QSize(tile_size, tile_size), QImage.Format_RGB32) self.settings = QgsMapSettings() self.settings.setOutputDpi(self.image.logicalDpiX()) self.settings.setOutputImageFormat(QImage.Format_RGB32) self.settings.setDestinationCrs(crs) self.settings.setOutputSize(self.image.size()) self.settings.setFlag(QgsMapSettings.Antialiasing, True) self.settings.setFlag(QgsMapSettings.RenderMapTile, True) if QgsProject.instance().mapThemeCollection().hasMapTheme(map_theme): self.settings.setLayers( QgsProject.instance().mapThemeCollection( ).mapThemeVisibleLayers( map_theme)) self.settings.setLayerStyleOverrides( QgsProject.instance().mapThemeCollection( ).mapThemeStyleOverrides( map_theme)) elif layer: self.settings.setLayers([layer]) else: self.settings.setLayers(map_settings.layers()) def render(self): for x in range(self.x_tile_count): for y in range(self.y_tile_count): cur_tile = x * self.y_tile_count + y num_tiles = self.x_tile_count * self.y_tile_count self.renderTile(x, y) def renderTile(self, x, y): """ Render one tile :param x: The x index of the current tile :param y: The y index of the current tile """ painter = QPainter(self.image) self.settings.setExtent(QgsRectangle( self.extent.xMinimum() + x * self.mupp * self.tile_size, self.extent.yMaximum() - (y + 1) * self.mupp * self.tile_size, self.extent.xMinimum() + (x + 1) * self.mupp * self.tile_size, self.extent.yMaximum() - y * self.mupp * self.tile_size)) job = QgsMapRendererCustomPainterJob(self.settings, painter) job.renderSynchronously() painter.end() # Needs not to be deleted or Windows will kill it too early... tmpfile = tempfile.NamedTemporaryFile(suffix='.png', delete=False) try: self.image.save(tmpfile.name) src_ds = osgeo.gdal.Open(tmpfile.name) self.dataset.WriteRaster(x * self.tile_size, y * self.tile_size, self.tile_size, self.tile_size, src_ds.ReadRaster(0, 0, self.tile_size, self.tile_size)) finally: del src_ds tmpfile.close() os.unlink(tmpfile.name) def getDriverForFile(self, filename): """ Get the GDAL driver for a filename, based on its extension. (.gpkg, .mbtiles...) """ _, extension = os.path.splitext(filename) # If no extension is set, use .tif as default if extension == '': extension = '.tif' for i in range(osgeo.gdal.GetDriverCount()): driver = osgeo.gdal.GetDriver(i) if driver.GetMetadataItem('DMD_EXTENSION') == extension[1:]: return driver