Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4998 from marioba/rasterize_provider
[FEATURE] Added processing algorithm to convert map to raster
- Loading branch information
Showing
4 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,325 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
""" | ||
/*************************************************************************** | ||
Rasterize.py | ||
------------------- | ||
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, | ||
QgsProcessingParameterMapLayer, | ||
QgsProcessingParameterRasterDestination, | ||
QgsMessageLog, | ||
QgsRasterFileWriter | ||
) | ||
|
||
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): | ||
"""Processing algorithm renders map canvas to a raster file. | ||
It's possible to choose the following parameters: | ||
- Map theme to render | ||
- Layer to render | ||
- The minimum extent to render | ||
- The tile size | ||
- Map unit per pixel | ||
- The output (can be saved to a file or to a temporary file and | ||
automatically opened as layer in qgis) | ||
""" | ||
|
||
# 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 = 'OUTPUT' | ||
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 | ||
map_theme_param = QgsProcessingParameterString( | ||
self.MAP_THEME, | ||
description=self.tr( | ||
'Map theme to render.'), | ||
defaultValue=None, optional=True) | ||
|
||
map_theme_param.setMetadata( | ||
{'widget_wrapper': { | ||
'class': | ||
'processing.gui.wrappers_map_theme.MapThemeWrapper'}}) | ||
self.addParameter(map_theme_param) | ||
|
||
self.addParameter( | ||
# TODO use QgsProcessingParameterMapLayer when | ||
# the LayerWidgetWrapper class will be implemented | ||
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, | ||
minValue=0, | ||
type=QgsProcessingParameterNumber.Double | ||
)) | ||
|
||
# We add a raster layer as output | ||
self.addParameter(QgsProcessingParameterRasterDestination( | ||
self.OUTPUT, | ||
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 tags(self): | ||
return self.tr('layer,raster,convert,file,map themes,tiles').split(',') | ||
|
||
# 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.parameterAsLayer( | ||
parameters, | ||
self.LAYER, | ||
context) | ||
|
||
extent = self.parameterAsExtent( | ||
parameters, | ||
self.EXTENT, | ||
context) | ||
|
||
tile_size = self.parameterAsInt( | ||
parameters, | ||
self.TILE_SIZE, | ||
context) | ||
|
||
mupp = self.parameterAsDouble( | ||
parameters, | ||
self.MAP_UNITS_PER_PIXEL, | ||
context) | ||
|
||
output_layer = self.parameterAsOutputLayer( | ||
parameters, | ||
self.OUTPUT, | ||
context) | ||
|
||
tile_set = TileSet(map_theme, layer, extent, tile_size, mupp, | ||
output_layer, | ||
qgis.utils.iface.mapCanvas().mapSettings()) | ||
tile_set.render(feedback) | ||
|
||
return {self.OUTPUT: output_layer} | ||
|
||
|
||
class TileSet(): | ||
""" | ||
A set of tiles | ||
""" | ||
|
||
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 | ||
|
||
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, feedback): | ||
for x in range(self.x_tile_count): | ||
for y in range(self.y_tile_count): | ||
if feedback.isCanceled(): | ||
return | ||
cur_tile = x * self.y_tile_count + y | ||
num_tiles = self.x_tile_count * self.y_tile_count | ||
self.renderTile(x, y, feedback) | ||
|
||
feedback.setProgress(int((cur_tile / num_tiles) * 100)) | ||
|
||
def renderTile(self, x, y, feedback): | ||
""" | ||
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)) | ||
except Exception as e: | ||
feedback.reportError(str(e)) | ||
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' | ||
|
||
driver_name = QgsRasterFileWriter.driverForExtension(extension[1:]) | ||
return osgeo.gdal.GetDriverByName(driver_name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.