Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added processing algorithm to convert map to raster
- Loading branch information
Showing
2 changed files
with
277 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,275 @@ | ||
# -*- 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 |
525aeab
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice feature. There's a problem when exporting a map with two layers (simple hillshade raster below a vector polygon layer with a blending mode set to multiply). Somehow, there are large black areas showing up:
525aeab
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I should have mentioned, the exported map-to-raster is the black square, I've zoomed out on the canvas to show how the canvas base raster is a light hillshade, which fails to export, and instead turns black when not overlapping a polygon layer.)