Skip to content

Commit a12346c

Browse files
authored
Merge pull request #4998 from marioba/rasterize_provider
[FEATURE] Added processing algorithm to convert map to raster
2 parents 425aa30 + d9f71de commit a12346c

File tree

4 files changed

+386
-0
lines changed

4 files changed

+386
-0
lines changed

python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
from .RandomPointsPolygons import RandomPointsPolygons
110110
from .RandomSelection import RandomSelection
111111
from .RandomSelectionWithinSubsets import RandomSelectionWithinSubsets
112+
from .Rasterize import RasterizeAlgorithm
112113
from .RasterLayerStatistics import RasterLayerStatistics
113114
from .RegularPoints import RegularPoints
114115
from .ReverseLineDirection import ReverseLineDirection
@@ -275,6 +276,7 @@ def getAlgs(self):
275276
RandomPointsPolygons(),
276277
RandomSelection(),
277278
RandomSelectionWithinSubsets(),
279+
RasterizeAlgorithm(),
278280
RasterLayerStatistics(),
279281
RegularPoints(),
280282
ReverseLineDirection(),
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
/***************************************************************************
5+
Rasterize.py
6+
-------------------
7+
begin : 2016-10-05
8+
copyright : (C) 2016 by OPENGIS.ch
9+
email : matthias@opengis.ch
10+
***************************************************************************/
11+
12+
/***************************************************************************
13+
* *
14+
* This program is free software; you can redistribute it and/or modify *
15+
* it under the terms of the GNU General Public License as published by *
16+
* the Free Software Foundation; either version 2 of the License, or *
17+
* (at your option) any later version. *
18+
* *
19+
***************************************************************************/
20+
"""
21+
22+
from processing.core.outputs import OutputRaster
23+
24+
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
25+
26+
from qgis.PyQt.QtGui import QImage, QPainter
27+
from qgis.PyQt.QtCore import QSize
28+
from qgis.core import (
29+
QgsMapSettings,
30+
QgsMapRendererCustomPainterJob,
31+
QgsRectangle,
32+
QgsProject,
33+
QgsProcessingException,
34+
QgsProcessingParameterExtent,
35+
QgsProcessingParameterString,
36+
QgsProcessingParameterNumber,
37+
QgsProcessingParameterRasterLayer,
38+
QgsProcessingParameterMapLayer,
39+
QgsProcessingParameterRasterDestination,
40+
QgsMessageLog,
41+
QgsRasterFileWriter
42+
)
43+
44+
import qgis
45+
import osgeo.gdal
46+
import os
47+
import tempfile
48+
import math
49+
50+
__author__ = 'Matthias Kuhn'
51+
__date__ = '2016-10-05'
52+
__copyright__ = '(C) 2016 by OPENGIS.ch'
53+
54+
# This will get replaced with a git SHA1 when you do a git archive
55+
56+
__revision__ = '$Format:%H$'
57+
58+
59+
class RasterizeAlgorithm(QgisAlgorithm):
60+
"""Processing algorithm renders map canvas to a raster file.
61+
It's possible to choose the following parameters:
62+
- Map theme to render
63+
- Layer to render
64+
- The minimum extent to render
65+
- The tile size
66+
- Map unit per pixel
67+
- The output (can be saved to a file or to a temporary file and
68+
automatically opened as layer in qgis)
69+
"""
70+
71+
# Constants used to refer to parameters and outputs. They will be
72+
# used when calling the algorithm from another algorithm, or when
73+
# calling from the QGIS console.
74+
75+
OUTPUT = 'OUTPUT'
76+
MAP_THEME = 'MAP_THEME'
77+
LAYER = 'LAYER'
78+
EXTENT = 'EXTENT'
79+
TILE_SIZE = 'TILE_SIZE'
80+
MAP_UNITS_PER_PIXEL = 'MAP_UNITS_PER_PIXEL'
81+
82+
def __init__(self):
83+
super().__init__()
84+
85+
def initAlgorithm(self, config=None):
86+
"""Here we define the inputs and output of the algorithm, along
87+
with some other properties.
88+
"""
89+
# The parameters
90+
map_theme_param = QgsProcessingParameterString(
91+
self.MAP_THEME,
92+
description=self.tr(
93+
'Map theme to render.'),
94+
defaultValue=None, optional=True)
95+
96+
map_theme_param.setMetadata(
97+
{'widget_wrapper': {
98+
'class':
99+
'processing.gui.wrappers_map_theme.MapThemeWrapper'}})
100+
self.addParameter(map_theme_param)
101+
102+
self.addParameter(
103+
# TODO use QgsProcessingParameterMapLayer when
104+
# the LayerWidgetWrapper class will be implemented
105+
QgsProcessingParameterRasterLayer(
106+
self.LAYER,
107+
description=self.tr(
108+
'Layer to render. Will only be used if the map theme '
109+
'is not set. '
110+
'If both, map theme and layer are not '
111+
'set, the current map content will be rendered.'),
112+
optional=True))
113+
self.addParameter(
114+
QgsProcessingParameterExtent(self.EXTENT, description=self.tr(
115+
'The minimum extent to render. Will internally be extended to '
116+
'be a multiple of the tile sizes.')))
117+
self.addParameter(
118+
QgsProcessingParameterNumber(
119+
self.TILE_SIZE,
120+
self.tr('Tile size'),
121+
defaultValue=1024))
122+
123+
self.addParameter(QgsProcessingParameterNumber(
124+
self.MAP_UNITS_PER_PIXEL,
125+
self.tr(
126+
'Map units per '
127+
'pixel'),
128+
defaultValue=100,
129+
minValue=0,
130+
type=QgsProcessingParameterNumber.Double
131+
))
132+
133+
# We add a raster layer as output
134+
self.addParameter(QgsProcessingParameterRasterDestination(
135+
self.OUTPUT,
136+
self.tr(
137+
'Output layer')))
138+
139+
def name(self):
140+
# Unique (non-user visible) name of algorithm
141+
return 'Rasterize'
142+
143+
def displayName(self):
144+
# The name that the user will see in the toolbox
145+
return self.tr('Convert map to raster')
146+
147+
def group(self):
148+
return self.tr('Raster tools')
149+
150+
def tags(self):
151+
return self.tr('layer,raster,convert,file,map themes,tiles').split(',')
152+
153+
# def processAlgorithm(self, progress):
154+
def processAlgorithm(self, parameters, context, feedback):
155+
"""Here is where the processing itself takes place."""
156+
157+
# The first thing to do is retrieve the values of the parameters
158+
# entered by the user
159+
map_theme = self.parameterAsString(
160+
parameters,
161+
self.MAP_THEME,
162+
context)
163+
164+
layer = self.parameterAsLayer(
165+
parameters,
166+
self.LAYER,
167+
context)
168+
169+
extent = self.parameterAsExtent(
170+
parameters,
171+
self.EXTENT,
172+
context)
173+
174+
tile_size = self.parameterAsInt(
175+
parameters,
176+
self.TILE_SIZE,
177+
context)
178+
179+
mupp = self.parameterAsDouble(
180+
parameters,
181+
self.MAP_UNITS_PER_PIXEL,
182+
context)
183+
184+
output_layer = self.parameterAsOutputLayer(
185+
parameters,
186+
self.OUTPUT,
187+
context)
188+
189+
tile_set = TileSet(map_theme, layer, extent, tile_size, mupp,
190+
output_layer,
191+
qgis.utils.iface.mapCanvas().mapSettings())
192+
tile_set.render(feedback)
193+
194+
return {self.OUTPUT: output_layer}
195+
196+
197+
class TileSet():
198+
"""
199+
A set of tiles
200+
"""
201+
202+
def __init__(self, map_theme, layer, extent, tile_size, mupp, output,
203+
map_settings):
204+
"""
205+
206+
:param map_theme:
207+
:param extent:
208+
:param layer:
209+
:param tile_size:
210+
:param mupp:
211+
:param output:
212+
:param map_settings: Map canvas map settings used for some fallback
213+
values and CRS
214+
"""
215+
216+
self.extent = extent
217+
self.mupp = mupp
218+
self.tile_size = tile_size
219+
220+
driver = self.getDriverForFile(output)
221+
222+
if not driver:
223+
raise QgsProcessingException(
224+
u'Could not load GDAL driver for file {}'.format(output))
225+
226+
crs = map_settings.destinationCrs()
227+
228+
self.x_tile_count = math.ceil(extent.width() / mupp / tile_size)
229+
self.y_tile_count = math.ceil(extent.height() / mupp / tile_size)
230+
231+
xsize = self.x_tile_count * tile_size
232+
ysize = self.y_tile_count * tile_size
233+
234+
self.dataset = driver.Create(output, xsize, ysize, 3) # 3 bands
235+
self.dataset.SetProjection(str(crs.toWkt()))
236+
self.dataset.SetGeoTransform(
237+
[extent.xMinimum(), mupp, 0, extent.yMaximum(), 0, -mupp])
238+
239+
self.image = QImage(QSize(tile_size, tile_size), QImage.Format_RGB32)
240+
241+
self.settings = QgsMapSettings()
242+
self.settings.setOutputDpi(self.image.logicalDpiX())
243+
self.settings.setOutputImageFormat(QImage.Format_RGB32)
244+
self.settings.setDestinationCrs(crs)
245+
self.settings.setOutputSize(self.image.size())
246+
self.settings.setFlag(QgsMapSettings.Antialiasing, True)
247+
self.settings.setFlag(QgsMapSettings.RenderMapTile, True)
248+
249+
if QgsProject.instance().mapThemeCollection().hasMapTheme(map_theme):
250+
self.settings.setLayers(
251+
QgsProject.instance().mapThemeCollection(
252+
253+
).mapThemeVisibleLayers(
254+
map_theme))
255+
self.settings.setLayerStyleOverrides(
256+
QgsProject.instance().mapThemeCollection(
257+
258+
).mapThemeStyleOverrides(
259+
map_theme))
260+
elif layer:
261+
self.settings.setLayers([layer])
262+
else:
263+
self.settings.setLayers(map_settings.layers())
264+
265+
def render(self, feedback):
266+
for x in range(self.x_tile_count):
267+
for y in range(self.y_tile_count):
268+
if feedback.isCanceled():
269+
return
270+
cur_tile = x * self.y_tile_count + y
271+
num_tiles = self.x_tile_count * self.y_tile_count
272+
self.renderTile(x, y, feedback)
273+
274+
feedback.setProgress(int((cur_tile / num_tiles) * 100))
275+
276+
def renderTile(self, x, y, feedback):
277+
"""
278+
Render one tile
279+
280+
:param x: The x index of the current tile
281+
:param y: The y index of the current tile
282+
"""
283+
painter = QPainter(self.image)
284+
285+
self.settings.setExtent(QgsRectangle(
286+
self.extent.xMinimum() + x * self.mupp * self.tile_size,
287+
self.extent.yMaximum() - (y + 1) * self.mupp * self.tile_size,
288+
self.extent.xMinimum() + (x + 1) * self.mupp * self.tile_size,
289+
self.extent.yMaximum() - y * self.mupp * self.tile_size))
290+
291+
job = QgsMapRendererCustomPainterJob(self.settings, painter)
292+
job.renderSynchronously()
293+
painter.end()
294+
295+
# Needs not to be deleted or Windows will kill it too early...
296+
tmpfile = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
297+
try:
298+
self.image.save(tmpfile.name)
299+
300+
src_ds = osgeo.gdal.Open(tmpfile.name)
301+
302+
self.dataset.WriteRaster(x * self.tile_size, y * self.tile_size,
303+
self.tile_size, self.tile_size,
304+
src_ds.ReadRaster(0, 0, self.tile_size,
305+
self.tile_size))
306+
except Exception as e:
307+
feedback.reportError(str(e))
308+
finally:
309+
del src_ds
310+
tmpfile.close()
311+
os.unlink(tmpfile.name)
312+
313+
def getDriverForFile(self, filename):
314+
"""
315+
Get the GDAL driver for a filename, based on its extension. (.gpkg,
316+
.mbtiles...)
317+
"""
318+
_, extension = os.path.splitext(filename)
319+
320+
# If no extension is set, use .tif as default
321+
if extension == '':
322+
extension = '.tif'
323+
324+
driver_name = QgsRasterFileWriter.driverForExtension(extension[1:])
325+
return osgeo.gdal.GetDriverByName(driver_name)

python/plugins/processing/gui/wrappers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,11 @@ def value(self):
754754
return self.widget.currentData()
755755

756756

757+
class LayerWidgetWrapper(WidgetWrapper):
758+
def __init__(self):
759+
raise NotImplementedError('Layer widget wrapper is not implemented yet')
760+
761+
757762
class VectorWidgetWrapper(WidgetWrapper):
758763

759764
NOT_SELECTED = '[Not selected]'
@@ -1388,6 +1393,8 @@ def create_wrapper_from_class(param, dialog, row=0, col=0):
13881393
wrapper = VectorWidgetWrapper
13891394
elif param.type() == 'band':
13901395
wrapper = BandWidgetWrapper
1396+
elif param.type() == 'layer':
1397+
wrapper = LayerWidgetWrapper
13911398
else:
13921399
assert False, param.type()
13931400
return wrapper(param, dialog, row, col)

0 commit comments

Comments
 (0)