Skip to content

Commit

Permalink
Added processing algorithm to convert map to raster
Browse files Browse the repository at this point in the history
  • Loading branch information
marioba committed Aug 8, 2017
1 parent 425aa30 commit 525aeab
Show file tree
Hide file tree
Showing 2 changed files with 277 additions and 0 deletions.
2 changes: 2 additions & 0 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -109,6 +109,7 @@
from .RandomPointsPolygons import RandomPointsPolygons
from .RandomSelection import RandomSelection
from .RandomSelectionWithinSubsets import RandomSelectionWithinSubsets
from .Rasterize import RasterizeAlgorithm
from .RasterLayerStatistics import RasterLayerStatistics
from .RegularPoints import RegularPoints
from .ReverseLineDirection import ReverseLineDirection
Expand Down Expand Up @@ -275,6 +276,7 @@ def getAlgs(self):
RandomPointsPolygons(),
RandomSelection(),
RandomSelectionWithinSubsets(),
RasterizeAlgorithm(),
RasterLayerStatistics(),
RegularPoints(),
ReverseLineDirection(),
Expand Down
275 changes: 275 additions & 0 deletions python/plugins/processing/algs/qgis/Rasterize.py
@@ -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

2 comments on commit 525aeab

@nirvn
Copy link
Contributor

@nirvn nirvn commented on 525aeab Aug 9, 2017

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:
screenshot from 2017-08-09 15-56-35

@nirvn
Copy link
Contributor

@nirvn nirvn commented on 525aeab Aug 9, 2017

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.)

Please sign in to comment.